<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Net Ninja &#8211; wordpress_blog</title>
	<atom:link href="/wordpress_blog/category/ninja/feed/" rel="self" type="application/rss+xml" />
	<link>/wordpress_blog</link>
	<description>This is a dynamic to static website.</description>
	<lastBuildDate>Wed, 26 Mar 2025 00:58:36 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.7.2</generator>

<image>
	<url>/wordpress_blog/wp-content/uploads/2022/03/logo.png</url>
	<title>Net Ninja &#8211; wordpress_blog</title>
	<link>/wordpress_blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>MERN Authentication Tutorial</title>
		<link>/wordpress_blog/mern-authentication-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Mon, 08 Jul 2024 09:29:35 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=865</guid>

					<description><![CDATA[Learning From Youtube Channel: N [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Net Ninja<br>Video:&nbsp;<a href="https://www.youtube.com/watch?v=WsRBmwNkv3Q&amp;list=PL4cUxeGkcC9g8OhpOZxNdhXggFz2lOuCT&amp;index=1" target="_blank" rel="noreferrer noopener">MERN Authentication Tutorial</a><br>Thank you.</p>



<h2 class="wp-block-heading">#1 – Intro &amp; Starter Project</h2>



<p>Learn how to implement authentication (using JSON web tokens), within the MERN stack, in this tutorial series.</p>



<ul class="wp-block-list">
<li>下載初始專案、講解專案檔案結構、內容</li>



<li>移動到 backend 資料夾 – cd backend</li>



<li>安裝 npm 套件 – npm install</li>



<li>移動到 frontend 資料夾 – cd frontend</li>



<li>安裝 npm 套件 – npm install</li>



<li>執行後端 backend – npm run dev</li>



<li>執行前端 frontend – npm start</li>
</ul>



<h2 class="wp-block-heading">#2 – User Routes, Controller &amp; Model</h2>



<p>In this MERN auth tutorial, you’ll make a user controller &amp; model, and set up some routes for authentication.</p>



<ul class="wp-block-list">
<li>在 routes 資料夾裡面建立 user.js 檔案</li>



<li>修改 user.js 檔案</li>



<li>在 controllers 資料夾裡面建立 userController.js 檔案</li>



<li>修改 userController.js 檔案</li>



<li>修改 user.js 檔案，controller functions</li>



<li>修改 server.js 檔案</li>



<li>在 models 資料夾裡面建立 userModel.js 檔案</li>



<li>修改 useModel.js 檔案</li>



<li>修改 userController.js 檔案，User</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/login，POST 方法<br>Body > raw > JSON</li>



<li>使用 API 測試工具<br>http://localhost:4000/api/user/signup，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/routes/user.js
const express = require('express')

// controller functions
const { signupUser, loginUser } = require('../controllers/userController')

const router = express.Router()

// login route
router.post('/login', loginUser)

// signup route
router.post('/signup', signupUser)

module.exports = router
</code></pre>



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')

// login user
const loginUser = async (req, res) =&gt; {
  res.json({mssg: 'login user'})
}

// signup user
const signupUser = async (req, res) =&gt; {
  res.json({mssg: 'signup user'})
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
require('dotenv').config()

const express = require('express')
const mongoose = require('mongoose')
const workoutRoutes = require('./routes/workouts')
const userRoutes = require('./routes/user')

// express app
const app = express()

// middleware
app.use(express.json())

app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.use('/api/workouts', workoutRoutes)
app.use('/api/user', userRoutes)

// connect to db
mongoose.connect(process.env.MONGO_URI)
  .then(() =&gt; {
    // listen for requests
    app.listen(process.env.PORT, () =&gt; {
      console.log('connected to db &amp; listening on port', process.env.PORT)
    })
  })
  .catch((error) =&gt; {
    console.log(error)
  })
</code></pre>



<pre class="wp-block-code"><code>// backend/models/userModel.js
const mongoose = require('mongoose')

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

module.exports = mongoose.model('User', userSchema)
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON - login
{
  "email": "yoshi@netninja.dev",
  "password": "abc123"
}
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON - signup
{
  "email": "yoshi@netninja.dev",
  "password": "abc123"
}
</code></pre>



<h2 class="wp-block-heading">#3 – Signing Up &amp; Hashing Passwords</h2>



<ul class="wp-block-list">
<li>修改 userModel.js 檔案，static signup method</li>



<li>安裝 bcrypt 套件 – npm install bcrypt<br>修改 userModel.js 檔案</li>



<li>修改 userController.js 檔案</li>



<li>修改 userModels.js 檔案，function</li>



<li>重新執行後端 server – nodemon server</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

// static signup method
userSchema.statics.signup = async function (email, password) {

  const exists = await this.findOne({ email })

  if (exists) {
    throw Error('Email already in use')
  }

  const salt = await bcrypt.genSalt(10)
  const hash = await bcrypt.hash(password, salt)

  const user = await this.create({ email, password: hash })

  return user
}

module.exports = mongoose.model('User', userSchema)
</code></pre>



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')

// login user
const loginUser = async (req, res) =&gt; {
  res.json({mssg: 'login user'})
}

// signup user
const signupUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.signup(email, password)

    res.status(200).json({ email, user })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "email": "yoshi@netninja.dev",
  "password": "abc123"
}
</code></pre>



<h2 class="wp-block-heading">#4 – Email &amp; Password Validation</h2>



<ul class="wp-block-list">
<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>使用錯誤的 email 格式測試，沒有完整格式、空值</li>



<li>安裝 validator 套件 – npm install validator</li>



<li>修改 userModel.js 檔案，validator</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>使用錯誤的 email 格式測試，沒有完整格式、空值</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const validator = require('validator')

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

// static signup method
userSchema.statics.signup = async function (email, password) {

  // validation
    if (!email || !password) {
      throw Error('All fields must be filled')
    }
    if (!validator.isEmail(email)) {
      throw Error('Email not valid')
    }
    if (!validator.isStrongPassword(password)) {
      throw Error('Password not strong enough')
    }

  const exists = await this.findOne({ email })

  if (exists) {
    throw Error('Email already in use')
  }

  const salt = await bcrypt.genSalt(10)
  const hash = await bcrypt.hash(password, salt)

  const user = await this.create({ email, password: hash })

  return user
}

module.exports = mongoose.model('User', userSchema)
</code></pre>



<h2 class="wp-block-heading">#5 – JSON Web Tokens (theory)</h2>



<p>In this MERN Authentication tutorial you’ll learn about JSON Web Tokens (JWT’s) and how they work under the hood when it comes to authentication.</p>



<ul class="wp-block-list">
<li>講解 JSON Web Tokens</li>



<li>介紹 JWT 的 Encoded、Decoded</li>



<li>講解 Header、Payload、Signature</li>
</ul>



<h3 class="wp-block-heading">JSON Web Tokens</h3>



<p>Header<br>Contains the algorithm used for the JWT</p>



<p>Payload<br>Contains non-sensitive user data (e.g. a user id)</p>



<p>Signature<br>Used to verify the token by the server</p>



<h2 class="wp-block-heading">#6 – Signing Tokens</h2>



<p>In this MERN Authentication tutorial, we’ll see how to sign tokens and send them back to the client.</p>



<ul class="wp-block-list">
<li>安裝 jsonwebtoken 套件 – npm install jsonwebtoken</li>



<li>修改 userController.js 檔案</li>



<li>修改 .env 檔案，避免敏感、重要的資料上傳到 GitHub</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')

const createToken = (_id) =&gt; {
  return jwt.sign({_id}, process.env.SECRET, { expiresIn: '3d' })
}

// login user
const loginUser = async (req, res) =&gt; {
  res.json({mssg: 'login user'})
}

// signup user
const signupUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.signup(email, password)

    // create a token
    const token = createToken(user._id)

    res.status(200).json({ email, token })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// backend/.env
PORT=4000
MONGO_URI=mongodb+srv://mario:test1234@mernapp.bldt4v0.mongodb.net/?retryWrites=true&amp;w=majority&amp;appName=MERNapp
SECRET =learnfromnetninjathankyou
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "email": "luigi@netninja.dev",
  "password": "ABCabc123!"
}
</code></pre>



<h2 class="wp-block-heading">#7 – Logging Users In</h2>



<ul class="wp-block-list">
<li>修改 userModel.js 檔案，static login method</li>



<li>修改 userController.js 檔案，login user</li>



<li>使用 API 測試工具 Postman，測試各種資料情形<br>http://localhost:4000/api/user/login，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const validator = require('validator')

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

// static signup method
userSchema.statics.signup = async function (email, password) {

  // validation
    if (!email || !password) {
      throw Error('All fields must be filled')
    }
    if (!validator.isEmail(email)) {
      throw Error('Email not valid')
    }
    if (!validator.isStrongPassword(password)) {
      throw Error('Password not strong enough')
    }

  const exists = await this.findOne({ email })

  if (exists) {
    throw Error('Email already in use')
  }

  const salt = await bcrypt.genSalt(10)
  const hash = await bcrypt.hash(password, salt)

  const user = await this.create({ email, password: hash })

  return user
}

// static login method
userSchema.statics.login = async function (email, password) {

  if (!email || !password) {
    throw Error('All fields must be filled')
  }

  const user = await this.findOne({ email })

  if (!user) {
    throw Error('Incorrect email')
  }

  const match = await bcrypt.compare(password, user.password)

  if (!match) {
    throw Error('Incorrect password')
  }

  return user
}

module.exports = mongoose.model('User', userSchema)
</code></pre>



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')

const createToken = (_id) =&gt; {
  return jwt.sign({_id}, process.env.SECRET, { expiresIn: '3d' })
}

// login user
const loginUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.login(email, password)

    // create a token
    const token = createToken(user._id)

    res.status(200).json({ email, token })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// signup user
const signupUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.signup(email, password)

    // create a token
    const token = createToken(user._id)

    res.status(200).json({ email, token })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "email": "luigi@netninja.dev",
  "password": "ABCabc123!"
}
</code></pre>



<h2 class="wp-block-heading">#8 – React Auth Context</h2>



<ul class="wp-block-list">
<li>在 context 資料夾裡面建立 AuthContext.js 檔案</li>



<li>修改 AuthContext.js 檔案</li>



<li>修改 index.js 檔案，匯入 AuthContextProvider</li>



<li>在 hook 資料夾裡面建立 useAuthContext.js 檔案</li>



<li>修改 useAuthContext.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// backend/src/context/AuthContext.js
import { createContext, useReducer } from 'react'

export const AuthContext = createContext()

export const authReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'LOGIN':
      return { user: action.payload }
    case 'LOGOUT':
      return { user: null}
    default:
      return state
  }
}

export const AuthContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(authReducer, {
    user: null
  })

  console.log('AuthContext State: ', state)

  return (
    &lt;AuthContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/AuthContext.Provider&gt;
  )
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { WorkoutsContextProvider } from './context/WorkoutContext';
import { AuthContextProvider } from './context/AuthContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  &lt;React.StrictMode&gt;
    &lt;AuthContextProvider&gt;
      &lt;WorkoutsContextProvider&gt;
        &lt;App /&gt;
      &lt;/WorkoutsContextProvider&gt;
    &lt;/AuthContextProvider&gt;
  &lt;/React.StrictMode&gt;
);
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/hooks/useAuthContext.js
import { AuthContext } from "../context/AuthContext";
import { useContext } from 'react'

export const useAuthContext = () =&gt; {
  const context = useContext(AuthContext)

  if (!context) {
    throw Error('useAuthContext must be used inside an AuthContextProvider')
  }

  return context
}
</code></pre>



<h2 class="wp-block-heading">#9 – Login &amp; Signup Forms</h2>



<p>In this MERN auth tutorial, we’ll flesh out the login and signup forms.</p>



<ul class="wp-block-list">
<li>在 pages 資料夾裡面建立 Signup.js 檔案</li>



<li>修改 Signup.js 檔案</li>



<li>在 pages 資料夾裡面建立 Login.js 檔案</li>



<li>修改 Login.js 檔案，從 Signup.js 複製修改</li>



<li>修改 App.js 檔案</li>



<li>修改 Navbar.js 檔案</li>



<li>修改 index.css 檔案</li>



<li>測試網頁表單 Sign up 關於 console 查詢顯示</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Signup.js
import { useState } from 'react'

const Signup = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    console.log(email, password)
  }

  return (
    &lt;form className='signup' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Sign up&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button&gt;Sign up&lt;/button&gt;
    &lt;/form&gt;
  )
}

export default Signup
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.js
import { useState } from 'react'

const Login = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    console.log(email, password)
  }

  return (
    &lt;form className='login' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Log in&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button&gt;Log in&lt;/button&gt;
    &lt;/form&gt;
  )
}

export default Login
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.js
import { BrowserRouter , Routes, Route } from 'react-router-dom'

// pages &amp; components
import Home from './pages/Home'
import Login from './pages/Login';
import Signup from './pages/Signup';
import Navbar from './components/Navbar'

function App() {
  return (
    &lt;div className="App"&gt;
        &lt;BrowserRouter&gt;
          &lt;Navbar /&gt;
          &lt;div className="pages"&gt;
            &lt;Routes&gt;
              &lt;Route
                path="/"
                element={&lt;Home /&gt;} 
              /&gt;
              &lt;Route
                path="/login"
                element={&lt;Login /&gt;} 
              /&gt;
              &lt;Route
                path="/signup"
                element={&lt;Signup /&gt;} 
              /&gt;
            &lt;/Routes&gt;
          &lt;/div&gt;
        &lt;/BrowserRouter&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"

const Navbar = () =&gt; {

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
        &lt;nav&gt;
          &lt;div&gt;
            &lt;Link to="/login"&gt;Login&lt;/Link&gt;
            &lt;Link to="/signup"&gt;Signup&lt;/Link&gt;
          &lt;/div&gt;
        &lt;/nav&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}

/* navbar */
nav {
  display: flex;
  align-items: center;
}

nav a {
  margin-left: 10px;
}

/* auth forms */
form.signup, form.login {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}
</code></pre>



<h2 class="wp-block-heading">#10 – Making a useSignup Hook</h2>



<p>In this MERN auth lesson, you’ll create a custom, reusable React hook that we can use to sign new users up to the application.</p>



<ul class="wp-block-list">
<li>在 hooks 資料夾裡面建立 useSignup.js 檔案</li>



<li>修改 useSignup.js 檔案</li>



<li>修改 Signup.js 檔案，useSignup</li>



<li>測試 Sign up 表單能否正常運作</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/hooks/useSignup.js
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useSignup = () =&gt; {
  const &#91;error, setError] = useState(null)
  const &#91;isLoading, setIsLoading] = useState(null)
  const { dispatch } = useAuthContext()

  const signup = async (email, password) =&gt; {
    setIsLoading(true)
    setError(null)

    const response = await fetch('/api/user/signup', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({email, password})
    })
    const json = await response.json()

    if (!response.ok) {
      setIsLoading(false)
      setError(json.error)
    }
    if (response.ok) {
      // save the user to local storage
      localStorage.setItem('user', JSON.stringify(json))

      // update the auth context
      dispatch({type: 'LOGIN', payload: json})

      setIsLoading(false)
    }
  }

  return { signup, isLoading, error}
} 
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Signup.js
import { useState } from 'react'
import { useSignup } from '../hooks/useSignup'

const Signup = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')
  const {signup, error, isLoading} = useSignup()

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    await signup(email, password)
  }

  return (
    &lt;form className='signup' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Sign up&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button disabled={isLoading}&gt;Sign up&lt;/button&gt;
      {error &amp;&amp; &lt;div className='error'&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default Signup
</code></pre>



<h2 class="wp-block-heading">#11 – Making a useLogout Hook</h2>



<p>In this MERN auth tutorial, we’ll make a custom useLogout hook to log users out of the application.</p>



<ul class="wp-block-list">
<li>在 hooks 資料夾裡面建立 useLogout.js 檔案</li>



<li>修改 useLogout.js 檔案</li>



<li>修改 Navbar.js 檔案</li>



<li>修改 index.css 檔案</li>



<li>測試 Log out 能否正確刪除 Local Storage 資料<br>console 檢視狀態</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/hooks/useLogout.js
import { useAuthContext } from './useAuthContext'

export const useLogout = () =&gt; {
  const { dispatch } = useAuthContext()

  const logout = () =&gt; {
    // remove user from storage
    localStorage.removeItem('user')

    // dispatch logout action
    dispatch({type: 'LOGOUT'})
  }

  return {logout}
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
import { useLogout } from '../hooks/useLogout'

const Navbar = () =&gt; {
  const { logout } = useLogout()

  const handleClick = () =&gt; {
    logout()
  }

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
        &lt;nav&gt;
          &lt;div&gt;
            &lt;button onClick={handleClick}&gt;Log out&lt;/button&gt;
          &lt;/div&gt;
          &lt;div&gt;
            &lt;Link to="/login"&gt;Login&lt;/Link&gt;
            &lt;Link to="/signup"&gt;Signup&lt;/Link&gt;
          &lt;/div&gt;
        &lt;/nav&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}

/* navbar */
nav {
  display: flex;
  align-items: center;
}

nav a {
  margin-left: 10px;
}

nav button {
  background: #fff;
  color: var(--primary);
  border: 2px solid var(--primary);
  padding: 6px 10px;
  border-radius: 4px;
  font-family: "Poppins";
  cursor: pointer;
  font-size: 1em;
}

/* auth forms */
form.signup, form.login {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}
</code></pre>



<h2 class="wp-block-heading">#12 – Making a useLogin Hook</h2>



<p>In this MERN auth tutorial, we’ll make a custom useLogin hook to log users in to the application.</p>



<ul class="wp-block-list">
<li>在 hooks 資料夾裡面建立 useLogin.js 檔案</li>



<li>修改 useLogin.js 檔案，複製 useSignup.js 程式碼做修改</li>



<li>修改 Login.js 檔案</li>



<li>註冊好帳號，測試登入帳號<br>講解 Local Storage、Token、Refresh</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/hooks/useLogin.js
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useLogin = () =&gt; {
  const &#91;error, setError] = useState(null)
  const &#91;isLoading, setIsLoading] = useState(null)
  const { dispatch } = useAuthContext()

  const login = async (email, password) =&gt; {
    setIsLoading(true)
    setError(null)

    const response = await fetch('/api/user/login', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({email, password})
    })
    const json = await response.json()

    if (!response.ok) {
      setIsLoading(false)
      setError(json.error)
    }
    if (response.ok) {
      // save the user to local storage
      localStorage.setItem('user', JSON.stringify(json))

      // update the auth context
      dispatch({type: 'LOGIN', payload: json})

      setIsLoading(false)
    }
  }

  return { login, isLoading, error}
} 
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.js
import { useState } from 'react'
import { useLogin } from '../hooks/useLogin'

const Login = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')
  const {login, error, isLoading} = useLogin()

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    await login(email, password)
  }

  return (
    &lt;form className='login' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Log in&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button disabled={isLoading}&gt;Log in&lt;/button&gt;
      {error &amp;&amp; &lt;div className='error'&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default Login
</code></pre>



<h2 class="wp-block-heading">#13 – Setting the Initial Auth Status</h2>



<ul class="wp-block-list">
<li>修改 Navbar.js 檔案，匯入 useAuthContext</li>



<li>測試登入表單確認功能是否正常</li>



<li>重整頁面後會回到登入頁面</li>



<li>修改 AuthContext.js 檔案，useEffect</li>



<li>重整頁面後仍會保持在登入狀態</li>



<li>測試登出、登入功能是否正常</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
import { useLogout } from '../hooks/useLogout'
import { useAuthContext } from "../hooks/useAuthContext"

const Navbar = () =&gt; {
  const { logout } = useLogout()
  const { user } = useAuthContext()

  const handleClick = () =&gt; {
    logout()
  }

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
        &lt;nav&gt;
          {user &amp;&amp; (
            &lt;div&gt;
              &lt;span&gt;{user.email}&lt;/span&gt;
              &lt;button onClick={handleClick}&gt;Log out&lt;/button&gt;
            &lt;/div&gt;
          )}
          {!user &amp;&amp; (
            &lt;div&gt;
              &lt;Link to="/login"&gt;Login&lt;/Link&gt;
              &lt;Link to="/signup"&gt;Signup&lt;/Link&gt;
            &lt;/div&gt;
          )}
        &lt;/nav&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/context/AuthContext.js
import { createContext, useReducer, useEffect } from 'react'

export const AuthContext = createContext()

export const authReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'LOGIN':
      return { user: action.payload }
    case 'LOGOUT':
      return { user: null}
    default:
      return state
  }
}

export const AuthContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(authReducer, {
    user: null
  })

  useEffect(() =&gt; {
    const user = JSON.parse(localStorage.getItem('user'))

    if (user) {
      dispatch({ type: 'LOGIN', payload: user })
    }
  }, &#91;])

  console.log('AuthContext State: ', state)

  return (
    &lt;AuthContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/AuthContext.Provider&gt;
  )
}
</code></pre>



<h2 class="wp-block-heading has-background" style="background-color:#cf2e2e">#14 – Protecting API Routes</h2>



<p>In this MERN auth tutorial, you’ll learn how to protect certain API routes from unauthenticated users.</p>



<ul class="wp-block-list">
<li>在 backend 資料夾建立 middleware 資料夾</li>



<li>在 middleware 資料夾裡面建立 requireAuth.js 檔案</li>



<li>查看 workouts.js 檔案</li>



<li>修改 requireAuth.js 檔案</li>



<li>修改 workouts.js 檔案，requireAuth</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/workouts/，GET 方法<br>Auth > Bearer > Bearer Token<br>http://localhost:4000/api/user/login，POST 方法<br>取得 token 資料</li>
</ul>



<pre class="wp-block-code"><code>// backend/middleware/requireAuth.js
const jwt = require('jsonwebtoken')
const User = require('../models/userModel')

const requireAuth = async (req, res, next) =&gt; {

  // verify authentication
  const { authorization } = req.headers

  if (!authorization) {
    return res.status(401).json({error: 'Authorization token required'})
  }

  const token = authorization.split(' ')&#91;1]

  try {
    const {_id} = jwt.verify(token, process.env.SECRET)

    req.user = await User.findOne({ _id }).select('_id')
    next()

  } catch (error) {
    console.log(error);
    res.status(401).json({error: 'Request is not authorized'})
  }

}

module.exports = requireAuth
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/workouts.js
const express = require('express')
const {
  createWorkout,
  getWorkouts,
  getWorkout,
  deleteWorkout,
  updateWorkout
} = require('../controllers/workoutController')
const requireAuth = require('../middleware/requireAuth')

const router = express.Router()

// require auth for all workout routes
router.use(requireAuth)

// GET all workouts
router.get('/', getWorkouts)

// GET a single workout
router.get('/:id', getWorkout)

// POST a new workout
router.post('/', createWorkout)

// DELETE a workout
router.delete('/:id', deleteWorkout)

// UPDATE a workout
router.patch('/:id', updateWorkout)

module.exports = router
</code></pre>



<h2 class="wp-block-heading">#15 – Making Authorized Requests</h2>



<ul class="wp-block-list">
<li>需要調整的有三個地方，Home.js、WorkoutForm.js、WorkoutDetails.js 檔案</li>



<li>修改 Home.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>使用 Add a New Workout 測試功能是否正常</li>



<li>登入帳號才能新增、刪除 Workout</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect } from 'react'
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'
import { useAuthContext } from '../hooks/useAuthContext'

// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'

const Home = () =&gt; {
  const {workouts, dispatch} = useWorkoutsContext()
  const {user} = useAuthContext()
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts', {
        headers: {
          'Authorization': `Bearer ${user.token}`
        }
      })
      const json = await response.json()

      if (response.ok) {
        dispatch({type: 'SET_WORKOUTS', payload: json})
      }
    }

    if (user) {
      fetchWorkouts()
    }
  }, &#91;dispatch, user])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
      &lt;WorkoutForm /&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"
import { useAuthContext } from "../hooks/useAuthContext"

const WorkoutForm = () =&gt; {
  const { dispatch } = useWorkoutsContext()
  const { user } = useAuthContext()

  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)
  const &#91;emptyFields, setEmptyFields] = useState(&#91;])

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    if (!user) {
      setError('You must be logged in')
      return
    }

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${user.token}`
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
      setEmptyFields(json.emptyFields)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      setEmptyFields(&#91;])
      console.log('new workout added', json);
      dispatch({type: 'CREATE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
        className={emptyFields.includes('title') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
        className={emptyFields.includes('load') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
        className={emptyFields.includes('reps') ? 'error' : ''}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'
import { useAuthContext } from '../hooks/useAuthContext'

// date fns
import formatDistanceToNow from 'date-fns/formatDistanceToNow'

const WorkoutDetails = ({ workout }) =&gt; {
  const { dispatch } = useWorkoutsContext()
  const { user } = useAuthContext()

  const handleClick = async () =&gt; {
    if (!user) {
      return
    }
    const response = await fetch('/api/workouts/' + workout._id, {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${user.token}`
      }
    })
    const json = await response.json()

    if (response.ok) {
      dispatch({type: 'DELETE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true })}&lt;/p&gt;
      &lt;span className='material-symbols-outlined' onClick={handleClick}&gt;delete&lt;/span&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails
</code></pre>



<h2 class="wp-block-heading">#16 – Protecting React Routes</h2>



<p>In this MERN auth tutorial we’ll protect some of the React routes from users that are not authenticated.</p>



<ul class="wp-block-list">
<li>修改 App.js 檔案，匯入 Navigate、useAuthContext</li>



<li>修改 index.css 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/App.js
import { BrowserRouter , Routes, Route, Navigate } from 'react-router-dom'
import { useAuthContext } from './hooks/useAuthContext'

// pages &amp; components
import Home from './pages/Home'
import Login from './pages/Login';
import Signup from './pages/Signup';
import Navbar from './components/Navbar'

function App() {
  const { user } = useAuthContext()

  return (
    &lt;div className="App"&gt;
        &lt;BrowserRouter&gt;
          &lt;Navbar /&gt;
          &lt;div className="pages"&gt;
            &lt;Routes&gt;
              &lt;Route
                path="/"
                element={user ? &lt;Home /&gt; : &lt;Navigate to="/login" /&gt;} 
              /&gt;
              &lt;Route
                path="/login"
                element={!user ? &lt;Login /&gt; : &lt;Navigate to="/" /&gt;} 
              /&gt;
              &lt;Route
                path="/signup"
                element={!user ? &lt;Signup /&gt; : &lt;Navigate to="/" /&gt;} 
              /&gt;
            &lt;/Routes&gt;
          &lt;/div&gt;
        &lt;/BrowserRouter&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}

/* navbar */
nav {
  display: flex;
  align-items: center;
}

nav a {
  margin-left: 10px;
}

nav button {
  background: #fff;
  color: var(--primary);
  border: 2px solid var(--primary);
  padding: 6px 10px;
  border-radius: 4px;
  font-family: "Poppins";
  cursor: pointer;
  font-size: 1em;
  margin-left: 10px;
}

/* auth forms */
form.signup, form.login {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}
</code></pre>



<h2 class="wp-block-heading">#17 – Assigning Workouts to Users</h2>



<ul class="wp-block-list">
<li>刪除所有的 Workouts</li>



<li>修改 workoutModel.js 檔案，user_id</li>



<li>修改 workoutController.js 檔案，user_id</li>



<li>增加 Workout 內容<br>目前還是會在不同帳號看到相同內容<br>分別在不同帳號增加 Workout 內容</li>



<li>使用 API 測試工具 Postman，GET 方法<br>http://localhost:4000/api/workouts/<br>必須加上 Bearer<br>可以看到 Response 的 user_id 的差異</li>



<li>修改 workoutController.js，get all workouts 加入 user_id</li>



<li>登入不同的帳號確認 Workout 內容<br>問題: 會短暫看到其他人的 Workout 內容</li>



<li>clear global workouts state<br>修改 useLogout.js 檔案，匯入 useWorkoutsContext</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/workoutModel.js
const mongoose = require('mongoose')

const Schema =  mongoose.Schema

const workoutSchema = new Schema({
  title: {
    type:String,
    required: true
  },
  reps: {
    type: Number,
    required: true
  },
  load: {
    type: Number,
    required: true
  },
  user_id: {
    type: String,
    required: true
  }
}, { timestamps: true })

module.exports = mongoose.model('Workout', workoutSchema)
</code></pre>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const user_id = req.user._id

  const workouts = await Workout.find({ user_id }).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  let emptyFields = &#91;]

  if(!title) {
    emptyFields.push('title')
  }
  if(!load) {
    emptyFields.push('load')
  }
  if(!reps) {
    emptyFields.push('reps')
  }
  if(emptyFields.length &gt; 0) {
    return res.status(400).json({ error: 'Please fill in all fields', emptyFields })
  }

  // add doc to db
  try {
    const user_id = req.user._id
    const workout = await Workout.create({title, load, reps, user_id})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout
const deleteWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndDelete({_id: id})

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}

// update a workout
const updateWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndUpdate({_id: id}, {
    ...req.body
  })

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout,
  deleteWorkout,
  updateWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/hooks/useLogout.js
import { useAuthContext } from './useAuthContext'
import { useWorkoutsContext } from './useWorkoutsContext'

export const useLogout = () =&gt; {
  const { dispatch } = useAuthContext()
  const { dispatch: workoutsDispatch } = useWorkoutsContext()

  const logout = () =&gt; {
    // remove user from storage
    localStorage.removeItem('user')

    // dispatch logout action
    dispatch({type: 'LOGOUT'})
    workoutsDispatch({type: 'SET_WORKOUTS', payload: null})
  }

  return {logout}
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>MERN Stack Tutorial</title>
		<link>/wordpress_blog/mern-stack-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Sat, 22 Jun 2024 04:57:04 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=862</guid>

					<description><![CDATA[Learning From Youtube Channel: N [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Net Ninja<br>Video: MERN Stack Tutorial<br>Thank you.</p>



<h2 class="wp-block-heading">#1 – What is the MERN Stack?</h2>



<p>Learn how to create a web app using the MERN stack (MongoDB, Express, React &amp; Node.js).</p>



<h3 class="wp-block-heading">MERN Stack</h3>



<ul class="wp-block-list">
<li>Mongo、Express、React、Node.js</li>



<li>講解 Front-end (browser) 和 Back-end (server) 關係</li>



<li>介紹製作的專案，這個課程不介紹 Auth 功能</li>
</ul>



<h3 class="wp-block-heading">Before You Start</h3>



<ul class="wp-block-list">
<li>Node.js、MongoDB、React 相關知識</li>
</ul>



<h3 class="wp-block-heading">安裝 node.js</h3>



<ul class="wp-block-list">
<li><a href="https://nodejs.org/en" target="_blank" rel="noreferrer noopener">Node.js</a> – 安裝長期穩定版本</li>



<li>查詢 Node.js 版本 – node -v</li>
</ul>



<h3 class="wp-block-heading">課程檔案資源</h3>



<ul class="wp-block-list">
<li><a href="https://github.com/iamshaunjp/MERN-Stack-Tutorial" target="_blank" rel="noreferrer noopener">Course Files</a></li>
</ul>



<h2 class="wp-block-heading">#2 – Express App Setup</h2>



<p>In this MERN tutorial we’ll set up our initial Express app to power the backend api.</p>



<h3 class="wp-block-heading">MERN Stack</h3>



<ul class="wp-block-list">
<li>建立 mern stack 專案資料夾</li>



<li>在 mern stack 資料夾裡面建立 backend 資料夾<br>也可以命名為 server 資料夾</li>



<li>在 backend 資料夾裡面建立 server.js 檔案</li>



<li>建立 package.json 檔案<br>使用終端機移動到 backend 資料夾 – cd backend<br>npm init -y 快速建立 package.json 檔案</li>



<li>安裝 express 套件 – npm install express</li>



<li>修改 server.js 檔案</li>



<li>使用終端機執行 server – node server.js</li>



<li>因為需要不斷重新執行 server 所以需要使用 nodemon 套件幫助網頁開發<br>安裝 nodemon 套件 – npm install -g nodemon</li>



<li>執行 server – nodemon server.js<br>可避免需要一直不斷重新啟動伺服器的動作</li>



<li>修改 package.json 檔案 – 新增 scripts 指令</li>



<li>執行 npm run dev 運作 server</li>



<li>修改 server.js 檔案，新增 routes</li>



<li>建立 .env 檔案，儲存環境變數<br>有需要上傳到 Github 可建立 .gitignore 檔案忽略文件</li>



<li>安裝 dotenv 套件 – npm install dotenv</li>



<li>修改 server.js 檔案，使用 dotenv</li>



<li>執行指令 npm run dev</li>



<li>介紹 API 測試工具 – Postman<br>http://localhost:4000/，GET 方法、建立 MERN app Collection</li>



<li>修改 server.js 檔案，middleware</li>
</ul>



<pre class="wp-block-code"><code>// backend/server.js
require('dotenv').config()

const express = require('express')

// express app
const app = express()

// middleware
app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.get('/', (req, res) =&gt; {
  res.json({mssg: 'Welcome to the app'})
})

// listen for requrests
app.listen(process.env.PORT, () =&gt; {
  console.log('listening on port', process.env.PORT);
})
</code></pre>



<pre class="wp-block-code"><code>// package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": &#91;],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.19.2"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// .env
PORT=4000
</code></pre>



<h2 class="wp-block-heading">#3 – Express Router &amp; API Routes</h2>



<p>In this MERN tutorial we’ll create all of the workout routes we need for the api and test them out using POSTMAN.</p>



<h3 class="wp-block-heading">API Endpoints</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>GET</td><td>/workouts</td><td>Gets all the workout documents</td></tr><tr><td>POST</td><td>/workouts</td><td>Creates a new workout document</td></tr><tr><td>GET</td><td>/workouts/:id</td><td>Gets a single workout document</td></tr><tr><td>DELETE</td><td>/workouts/:id</td><td>Deletes a single workout</td></tr><tr><td>PATCH</td><td>/workouts/:id</td><td>Updates a single workout</td></tr></tbody></table></figure>



<ul class="wp-block-list">
<li>在 backend 資料夾裡面建立 routes 資料夾</li>



<li>在 routes 資料夾裡面建立 workouts.js 檔案</li>



<li>修改 workouts.js 檔案</li>



<li>修改 server.js 檔案，workoutRoutes</li>



<li>使用 API 測試工具 – POSTMAN<br>也可以使用其他 API 測試工具</li>
</ul>



<pre class="wp-block-code"><code>// backend/routes/workouts.js
const express = require('express')

const router = express.Router()

// GET all workouts
router.get('/', (req, res) =&gt; {
  res.json({mssg: 'GET all workouts'})
})

// GET a single workout
router.get('/:id', (req, res) =&gt; {
  res.json({mssg: 'GET a single workout'})
})

// POST a new workout
router.post('/', (req, res) =&gt; {
  res.json({mssg: 'POST a new workout'})
})

// DELETE a workout
router.delete('/:id', (req, res) =&gt; {
  res.json({mssg: 'DELETE a workout'})
})

// UPDATE a workout
router.patch('/:id', (req, res) =&gt; {
  res.json({mssg: 'UPDATE a workout'})
})

module.exports = router
</code></pre>



<pre class="wp-block-code"><code>// server.js
require('dotenv').config()

const express = require('express')
const workoutRoutes = require('./routes/workouts')

// express app
const app = express()

// middleware
app.use(express.json())

app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.use('/api/workouts', workoutRoutes)

// listen for requests
app.listen(process.env.PORT, () =&gt; {
  console.log('listening on port', process.env.PORT)
})
</code></pre>



<h2 class="wp-block-heading">#4 – MongoDB Altas &amp; Mongoose</h2>



<p>In this MERN tutorial we’ll set up a database using MongoDB Atlas, and then connect to it from our application using a package called Mongoose.</p>



<ul class="wp-block-list">
<li>MongoDB Atlas</li>



<li>建立 Shared Cluster</li>



<li>Free Shared</li>



<li>Cloud Provider &amp; Region<br>aws、地區可自行選擇</li>



<li>Cluster Name: MERNapp</li>



<li>SECURITY > Database Access<br>Username and Password</li>



<li>SECURITY > Network Access</li>



<li>DEPLOYMENT > Database > Connect<br>Connect to your application<br>Add your connection string into your application code 複製</li>



<li>修改 .env 檔案</li>



<li>安裝 mongoose 套件 – npm install mongoose</li>



<li>修改 server.js 檔案</li>



<li>執行 npm run dev</li>
</ul>



<pre class="wp-block-code"><code>// .env
PORT=4000
MONGO_URI="connection string"
</code></pre>



<pre class="wp-block-code"><code>// server.js
require('dotenv').config()

const express = require('express')
const mongoose = require('mongoose')
const workoutRoutes = require('./routes/workouts')

// express app
const app = express()

// middleware
app.use(express.json())

app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.use('/api/workouts', workoutRoutes)

// connect to db
mongoose.connect(process.env.MONGO_URI)
  .then(() =&gt; {
    // listen for requests
    app.listen(process.env.PORT, () =&gt; {
      console.log('connected to db &amp; listening on port', process.env.PORT)
    })
  })
  .catch((error) =&gt; {
    console.log(error)
  })
</code></pre>



<h2 class="wp-block-heading">#5 – Models &amp; Schemas</h2>



<p>In this MERN tutorial you’ll create a new model &amp; schema for the dtabase collection we’ll be using (workouts).</p>



<ul class="wp-block-list">
<li>建立 models 資料夾</li>



<li>在 models 資料夾裡面建立 workoutModel.js 檔案</li>



<li>修改 workoutModel.js 檔案</li>



<li>修改 workouts.js 檔案</li>



<li>打開終端機查看 server.js 是否正常執行</li>



<li>使用 API 測試工具 Postman<br>localhost:4000/api/workouts/、POST 方法<br>Body > raw > JSON</li>



<li>講解物件少了一個屬性會產生錯誤</li>



<li>使用 MongoDB Atlas<br>DEPLOYMENT > Database > Browse Collections<br>查看是否有正確儲存到線上資料庫</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/workoutModel.js
const mongoose = require('mongoose')

const Schema =  mongoose.Schema

const workoutSchema = new Schema({
  title: {
    type:String,
    required: true
  },
  reps: {
    type: Number,
    required: true
  },
  load: {
    type: Number,
    required: true
  }
}, { timestamps: true })

module.exports = mongoose.model('Workout', workoutSchema)
</code></pre>



<pre class="wp-block-code"><code>// routes/workouts.js
const express = require('express')
const Workout = require('../models/workoutModel')

const router = express.Router()

// GET all workouts
router.get('/', (req, res) =&gt; {
  res.json({mssg: 'GET all workouts'})
})

// GET a single workout
router.get('/:id', (req, res) =&gt; {
  res.json({mssg: 'GET a single workout'})
})

// POST a new workout
router.post('/', async (req, res) =&gt; {
  const { title, load, reps } = req.body

  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// DELETE a workout
router.delete('/:id', (req, res) =&gt; {
  res.json({mssg: 'DELETE a workout'})
})

// UPDATE a workout
router.patch('/:id', (req, res) =&gt; {
  res.json({mssg: 'UPDATE a workout'})
})

module.exports = router
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "title": "Situps",
  "load": 0,
  "reps": 50
}
</code></pre>



<h2 class="wp-block-heading">#6 – Controllers (part 1)</h2>



<p>In this MERN tutorial we’ll make some controller functions for the workout routes.</p>



<ul class="wp-block-list">
<li>在 backend 資料夾裡面建立 controllers 資料夾</li>



<li>在 controllers 資料夾裡面建立 workoutController.js 檔案</li>



<li>修改 workouts.js 檔案</li>



<li>修改 workoutController.js 檔案</li>



<li>使用 API 測試工具 Postman 測試<br>localhost:4000/api/workouts/，POST 方法<br>新增資料內容<br>Body > raw > JSON</li>



<li>複製剛建立好的資料庫資料的id</li>



<li>使用 API 測試工具<br>http://localhost:4000/api/workouts/，GET 方法</li>



<li>使用 API 測試工具取得特定資料<br>http://localhost:4000/api/workouts/666ffbd9a0facf510722c94c，GET 方法<br>http://localhost:4000/api/workouts/43423，GET 方法</li>



<li>修改 workoutController.js 檔案，mongoose</li>



<li>再次使用 API 測試工具測試</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const workouts = await Workout.find({}).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  // add doc to db
  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout


// update a workout


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// backend/workouts.js
const express = require('express')
const {
  createWorkout,
  getWorkouts,
  getWorkout
} = require('../controllers/workoutController')

const router = express.Router()

// GET all workouts
router.get('/', getWorkouts)

// GET a single workout
router.get('/:id', getWorkout)

// POST a new workout
router.post('/', createWorkout)

// DELETE a workout
router.delete('/:id', (req, res) =&gt; {
  res.json({mssg: 'DELETE a workout'})
})

// UPDATE a workout
router.patch('/:id', (req, res) =&gt; {
  res.json({mssg: 'UPDATE a workout'})
})

module.exports = router
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "title": "Bench press",
  "load": 20,
  "reps": 40
}
</code></pre>



<h2 class="wp-block-heading">#7 – Controllers (part 2)</h2>



<p>In this MERN auth tutorial we’ll finish off the controller functions that we started in the last lesson.</p>



<ul class="wp-block-list">
<li>修改 workoutController.js 檔案，delete、update</li>



<li>修改 workouts.js 檔案</li>



<li>使用 API 測試工具 Postman，PATCH 方法<br>取得其中一筆資料 id<br>http://localhost:4000/api/workouts/666ffbd9a0facf510722c94c，PATCH 方法<br>Body > raw > JSON</li>



<li>使用 API 測試工具，GET 方法<br>查看資料是否有修改成功</li>



<li>使用 API 測試工具 Postman，DELETE 方法<br>取得其中一筆要刪除資料的 id<br>http://localhost:4000/api/workouts/666ffbd9a0facf510722c94c，DELETE 方法</li>



<li>使用 API 測試工具，GET 方法<br>查看資料是否有刪除成功</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const workouts = await Workout.find({}).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  // add doc to db
  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout
const deleteWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndDelete({_id: id})

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}

// update a workout
const updateWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndUpdate({_id: id}, {
    ...req.body
  })

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout,
  deleteWorkout,
  updateWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/workouts.js
const express = require('express')
const {
  createWorkout,
  getWorkouts,
  getWorkout,
  deleteWorkout,
  updateWorkout
} = require('../controllers/workoutController')

const router = express.Router()

// GET all workouts
router.get('/', getWorkouts)

// GET a single workout
router.get('/:id', getWorkout)

// POST a new workout
router.post('/', createWorkout)

// DELETE a workout
router.delete('/:id', deleteWorkout)

// UPDATE a workout
router.patch('/:id', updateWorkout)

module.exports = router
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "reps": 50
}
</code></pre>



<h2 class="wp-block-heading">#8 – Making a React App</h2>



<p>In this MERN tutorial, we’ll start our React application and set up a homepage route.</p>



<ul class="wp-block-list">
<li>終止後端 server 運行</li>



<li>在 mern stack 專案下建立 React App<br>npx create-react-app frontend</li>



<li>刪除 App.css、App.test.js、logo.svg、reportWebVitals.js、setupTests.js 檔案</li>



<li>修改 index.js 檔案</li>



<li>修改 App.js 檔案</li>



<li>安裝 react router 套件<br>移動到 frontend 資料夾位置<br>npm install react-router-dom</li>



<li>修改 App.js 檔案，匯入 react-router-dom</li>



<li>在 src 資料夾裡面建立 pages 資料夾</li>



<li>在 pages 資料夾裡面建立 Home.js 檔案</li>



<li>修改 Home.js 檔案</li>



<li>修改 App.js 檔案</li>



<li>打開終端機執行指令 – npm start<br>http://localhost:3000/</li>



<li>在 src 資料夾裡面建立 components 資料夾</li>



<li>在 components 資料夾裡面建立 Navbar.js 檔案</li>



<li>修改 Navbar.js 檔案</li>



<li>修改 App.js 檔案，匯入 Navbar</li>



<li>修改 index.css 檔案</li>



<li>檢查網頁畫面是否正常</li>
</ul>



<pre class="wp-block-code"><code>//  frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
)
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom'

// pages &amp; components
import Home from './pages/Home'
import Navbar from './components/Navbar'

function App() {

  return (
    &lt;div className="App"&gt;
      &lt;BrowserRouter&gt;
        &lt;Navbar /&gt;
        &lt;div className="pages"&gt;
          &lt;Routes&gt;
            &lt;Route 
              path="/" 
              element={&lt;Home /&gt;} 
            /&gt;
          &lt;/Routes&gt;
        &lt;/div&gt;
      &lt;/BrowserRouter&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>



<pre class="wp-block-code"><code>// forntend/src/pages/Home.js

const Home = () =&gt; {

  return (
    &lt;div className="home"&gt;
      &lt;h2&gt;Home&lt;/h2&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from 'react-router-dom'

const Navbar = () =&gt; {

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}
</code></pre>



<h2 class="wp-block-heading">#9 – Fetching Data</h2>



<p>In this MERN stack tutorial we’ll make a request to the backend api to fetch workouts data and output it in the React template.</p>



<ul class="wp-block-list">
<li>修改 Home.js 檔案</li>



<li>運行後端 server<br>移動到 backend 資料夾<br>終端機指令運行 npm run dev</li>



<li>查看 Google Console，有 Cors 錯誤</li>



<li>修改 package.json 檔案，新增 proxy 屬性、值</li>



<li>修改 Home.js 檔案</li>



<li>前端終端機重新運行</li>



<li>在 components 資料夾裡面建立 WorkoutDetails.js 檔案</li>



<li>修改 Home.js 檔案</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>注意: 需加上 return 才不會出現錯誤</li>



<li>修改 index.css 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect, useState } from 'react'

// components
import WorkoutDetails from '../components/WorkoutDetails'

const Home = () =&gt; {
  const &#91;workouts, setWorkouts] = useState(null)
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts')
      const json = await response.json()

      if (response.ok) {
        setWorkouts(json)
      }
    }

    fetchWorkouts()
  }, &#91;])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/package.json
{
  "proxy": "http://localhost:4000/",
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.23.1",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": &#91;
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": &#91;
      "&gt;0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": &#91;
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
const WorkoutDetails = ({ workout }) =&gt; {
  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{workout.createdAt}&lt;/p&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}
</code></pre>



<h2 class="wp-block-heading">#10 – New Workout Form</h2>



<p>In this lesson we’ll make a form to add new workouts.</p>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 WorkoutForm.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>查看後端 server.js 檔案，routes</li>



<li>修改 WorkoutForm.js 檔案，response</li>



<li>查看後端 workoutController.js 檔案</li>



<li>修改 WorkoutForm.js 檔案，error</li>



<li>修改 Home.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>在網頁測試增加功能，資料不完整與完整的差別</li>



<li>修改 index.css 檔案</li>



<li>在網頁測試增加功能，樣式修改後的變化</li>
</ul>



<pre class="wp-block-code"><code>// forntend/src/components/WorkoutForm.js
import { useState } from "react"

const WorkoutForm = () =&gt; {
  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      console.log('new workout added', json);
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect, useState } from 'react'

// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'

const Home = () =&gt; {
  const &#91;workouts, setWorkouts] = useState(null)
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts')
      const json = await response.json()

      if (response.ok) {
        setWorkouts(json)
      }
    }

    fetchWorkouts()
  }, &#91;])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
      &lt;WorkoutForm /&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}
</code></pre>



<h2 class="wp-block-heading has-background" style="background-color:#cf2e2e">#11 – Adding React Context</h2>



<p>In this MERN tutorial we’ll create a React Context to provide some global workouts state to the entire React application</p>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 context 資料夾</li>



<li>在 context 資料夾裡面建立 WorkoutContext.js 檔案</li>



<li>修改 WorkoutContext.js 檔案</li>



<li>修改 index.js 檔案</li>



<li>修改 WorkoutContext.js 檔案</li>



<li>在 src 資料夾裡面建立 hooks 資料夾</li>



<li>在 hooks 資料夾裡面建立 useWorkoutsContext.js 檔案</li>



<li>修改 useWorkoutsContext.js 檔案</li>



<li>講解 Home.js 檔案</li>



<li>講解 WorkoutContext.js 檔案</li>



<li>修改 Home.js 檔案，匯入 useWorkoutsContext、移除 useState</li>



<li>修改 WorkoutForm.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/context/WorkoutContext.js
import { createContext, useReducer } from 'react'

export const WorkoutsContext = createContext()

export const workoutsReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'SET_WORKOUTS':
      return {
        workouts: action.payload
      }
    case 'CREATE_WORKOUT':
      return {
        workouts: &#91;action.payload, ...state.workouts]
      }
    default:
      return state
  }
}

export const WorkoutsContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(workoutsReducer, {
    workouts: null
  })



  return (
    &lt;WorkoutsContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/WorkoutsContext.Provider&gt;
  )
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { WorkoutsContextProvider } from './context/WorkoutContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  &lt;React.StrictMode&gt;
    &lt;WorkoutsContextProvider&gt;
      &lt;App /&gt;
    &lt;/WorkoutsContextProvider&gt;
  &lt;/React.StrictMode&gt;
);
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/hooks/useWorkoutsContext.js
import { WorkoutsContext } from "../context/WorkoutContext";
import { useContext } from 'react'

export const useWorkoutsContext = () =&gt; {
  const context = useContext(WorkoutsContext)

  if (!context) {
    throw Error('useWorkoutsContext must be used inside an WorkoutsContextProvider')
  }

  return context
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect } from 'react'
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'

// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'

const Home = () =&gt; {
  const {workouts, dispatch} = useWorkoutsContext()
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts')
      const json = await response.json()

      if (response.ok) {
        dispatch({type: 'SET_WORKOUTS', payload: json})
      }
    }

    fetchWorkouts()
  }, &#91;dispatch])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
      &lt;WorkoutForm /&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"

const WorkoutForm = () =&gt; {
  const { dispatch } = useWorkoutsContext()

  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      console.log('new workout added', json);
      dispatch({type: 'CREATE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<h2 class="wp-block-heading">#12 – Deleting Data</h2>



<ul class="wp-block-list">
<li>修改 WorkoutDetails.js 檔案</li>



<li>修改 WorkoutContext.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'

const WorkoutDetails = ({ workout }) =&gt; {
  const { dispatch } = useWorkoutsContext()

  const handleClick = async () =&gt; {
    const response = await fetch('/api/workouts/' + workout._id, {
      method: 'DELETE'
    })
    const json = await response.json()

    if (response.ok) {
      dispatch({type: 'DELETE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{workout.createdAt}&lt;/p&gt;
      &lt;span onClick={handleClick}&gt;delete&lt;/span&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/context/WorkoutContext.js
import { createContext, useReducer } from 'react'

export const WorkoutsContext = createContext()

export const workoutsReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'SET_WORKOUTS':
      return {
        workouts: action.payload
      }
    case 'CREATE_WORKOUT':
      return {
        workouts: &#91;action.payload, ...state.workouts]
      }
    case 'DELETE_WORKOUT':
      return {
        workouts: state.workouts.filter((w) =&gt; w._id !== action.payload._id)
      }
    default:
      return state
  }
}

export const WorkoutsContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(workoutsReducer, {
    workouts: null
  })

  return (
    &lt;WorkoutsContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/WorkoutsContext.Provider&gt;
  )
}
</code></pre>



<h2 class="wp-block-heading">#13 – Handling Error Responses</h2>



<ul class="wp-block-list">
<li>講解後端 workoutController.js 檔案</li>



<li>講解後端 workoutModel.js 檔案</li>



<li>修改 workoutController.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>修改 index.css 檔案</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const workouts = await Workout.find({}).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  let emptyFields = &#91;]

  if(!title) {
    emptyFields.push('title')
  }
  if(!load) {
    emptyFields.push('load')
  }
  if(!reps) {
    emptyFields.push('reps')
  }
  if(emptyFields.length &gt; 0) {
    return res.status(400).json({ error: 'Please fill in all fields', emptyFields })
  }

  // add doc to db
  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout
const deleteWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndDelete({_id: id})

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}

// update a workout
const updateWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndUpdate({_id: id}, {
    ...req.body
  })

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout,
  deleteWorkout,
  updateWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"

const WorkoutForm = () =&gt; {
  const { dispatch } = useWorkoutsContext()

  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)
  const &#91;emptyFields, setEmptyFields] = useState(&#91;])

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
      setEmptyFields(json.emptyFields)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      setEmptyFields(&#91;])
      console.log('new workout added', json);
      dispatch({type: 'CREATE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
        className={emptyFields.includes('title') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
        className={emptyFields.includes('load') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
        className={emptyFields.includes('reps') ? 'error' : ''}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}
</code></pre>



<h2 class="wp-block-heading">#14 – Finishing Touches</h2>



<ul class="wp-block-list">
<li>修改 index.html 檔案，匯入 Google Material Symbols &amp; Icons</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>測試刪除按鈕是否能正常運作</li>



<li>使用、安裝 date-fns 套件<br>npm install date-fns</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>修改 Home.js 檔案 (Debug)，dispatch 之前已自行修正</li>
</ul>



<pre class="wp-block-code"><code>// frontend/public/index.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;link rel="icon" href="%PUBLIC_URL%/favicon.ico" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1" /&gt;
    &lt;meta name="theme-color" content="#000000" /&gt;
    &lt;meta
      name="description"
      content="Web site created using create-react-app"
    /&gt;
    &lt;link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /&gt;
    &lt;!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    --&gt;
    &lt;link rel="manifest" href="%PUBLIC_URL%/manifest.json" /&gt;
    &lt;!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    --&gt;
    &lt;link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" /&gt;
    &lt;title&gt;React App&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;noscript&gt;You need to enable JavaScript to run this app.&lt;/noscript&gt;
    &lt;div id="root"&gt;&lt;/div&gt;
    &lt;!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the &lt;body&gt; tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    --&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'

// date fns
import formatDistanceToNow from 'date-fns/formatDistanceToNow'

const WorkoutDetails = ({ workout }) =&gt; {
  const { dispatch } = useWorkoutsContext()

  const handleClick = async () =&gt; {
    const response = await fetch('/api/workouts/' + workout._id, {
      method: 'DELETE'
    })
    const json = await response.json()

    if (response.ok) {
      dispatch({type: 'DELETE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true })}&lt;/p&gt;
      &lt;span className='material-symbols-outlined' onClick={handleClick}&gt;delete&lt;/span&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Complete MongoDB Tutorial</title>
		<link>/wordpress_blog/complete-mongodb-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 04 Oct 2023 03:55:22 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=764</guid>

					<description><![CDATA[#1 – What is MongoDB? In this se [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">#1 – What is MongoDB?</h2>



<p>In this series you’ll learn how to use MongoDB (a NoSQL database) from scratch. You’ll also learn how to integrate it into a simple Node.js API.</p>



<h2 class="wp-block-heading">#2 – Installing MongoDB</h2>



<p>Learn how to install MongoDB locally onto your computer.</p>



<ul class="wp-block-list">
<li><a href="https://www.mongodb.com/try/download/community" target="_blank" rel="noreferrer noopener">MongoDB Community Server Download</a></li>



<li><a href="https://www.mongodb.com/try/download/shell" target="_blank" rel="noreferrer noopener">TOOLS – MongoDB Shell Download</a></li>
</ul>



<h2 class="wp-block-heading">#3 – Collections &amp; Documents</h2>



<h2 class="wp-block-heading">#4 – Using MongoDB Compass</h2>



<h2 class="wp-block-heading">#5 – Using the MongoDB Shell</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>show dbs</li>



<li>use database’s</li>
</ul>



<h3 class="wp-block-heading">使用其他終端機 – 指令</h3>



<ul class="wp-block-list">
<li>mongosh</li>



<li>cls</li>



<li>db</li>



<li>show collections</li>



<li>help</li>



<li>exit</li>
</ul>



<h2 class="wp-block-heading">#6 – Adding New Documents</h2>



<h3 class="wp-block-heading">指令</h3>



<pre class="wp-block-code"><code>// insertOne
db.books.insertOne({title: "The Color of Magic", author: "Terry Pratchett", pages: 300, rating: 7, genres: &#91;"fantasy", "magic"]})</code></pre>



<pre class="wp-block-code"><code>// insertMany
db.books.insertMany(&#91;{title: "The Light Fantastic", author: "Terry Pratchett", pages: 250, rating: 6, genres: &#91;"fantasy"]}, {title: "Dune", author: "Frank Herbert", pages: 500, rating: 10, genres: &#91;"sci-fi", "dystopian"]}])
</code></pre>



<h2 class="wp-block-heading">#7 – Finding Documents</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.find()</li>



<li>db.books.find({author: “Terry Pratchett”})</li>



<li>db.books.find({author: “Terry Pratchett”, rating: 7})</li>



<li>db.books.find({author: “Brandon Sanderson”})</li>



<li>db.books.find({author: “Brandon Sanderson”}, {title: 1, author: 1})</li>



<li>db.books.find({}, {title: 1, author: 1})</li>



<li>db.books.find({_id: ObjectId(“650be28b103e76e586910c81”)})</li>
</ul>



<h2 class="wp-block-heading">#8 – Sorting &amp; Limiting Data</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.find().count()</li>



<li>db.books.find({ author: “Brandon Sanderson” }).count()</li>



<li>db.books.find().limit(3)</li>



<li>db.books.find().limit(3).count()</li>



<li>db.books.find().sort({ title: 1 })</li>



<li>db.books.find().sort({ title: -1 })</li>



<li>db.books.find().sort({ title: 1 }).limit(3)</li>
</ul>



<h2 class="wp-block-heading">#9 – Nested Documents</h2>



<h3 class="wp-block-heading">指令</h3>



<pre class="wp-block-code"><code>// insertOne
db.books.insertOne({title: "The Way of Kings", author: "Brandon Sanderson", rating: 9, pages: 400, genres: &#91;"fantasy"], reviews: &#91;{name: "Yoshi", body: "Great book!!"}, {name: "mario", body: "so so"}]})</code></pre>



<pre class="wp-block-code"><code>// insertMany
db.books.insertMany(&#91;{title: "The Light Fantastic", author: "Terry Pratchett", pages: 250, rating: 6, genres: &#91;"fantasy", "magic"], reviews: &#91;{name:"luigi", body: "it was pretty good"}, {name: "bowser", body: "loved it!!"}]}, {title: "The Name of the Wind", "author": "Patrick Rothfuss", page: 500, "rating": 10, genres: &#91;"fantasy"], review: &#91;{name: "peach", body: "one of my favs"}]}, {title: "The Color of Magic", "author": "Terry Pratchett", page: 350, "rating": 8, genres: &#91;"fantasy", "magic"], review: &#91;{name: "luigi", body: "it was ok"}, {name: "bowser", body: "really good book"}]}, {title: "1984", "author": "George Orwell", page: 300, "rating": 6, genres: &#91;"sci-fi", "dystopian"], review: &#91;{name: "peach", body: "not my cup of tea"}, {name: "mario", body: "meh"}]}])</code></pre>



<h2 class="wp-block-heading">#10 – Operators &amp; Complex Queries</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.find({rating: 7})</li>



<li>db.books.find({ rating: {$gt: 7}})</li>



<li>db.books.find({ rating: {$gt: 8}})</li>



<li>db.books.find({ rating: {$lte: 8}})</li>



<li>db.books.find({ rating: {$gte: 8}})</li>



<li>db.books.find({ rating: {$gt: 7}, author: “Patrick Rothfuss”})</li>



<li>db.books.find({$or: [{rating: 7}, {rating: 9}]})</li>



<li>db.books.find({$or: [{rating: 7}, {author: “Terry Pratchett”}]})</li>



<li>db.books.find({$or: [{pages: {$lt: 300}}, {pages: {$gt: 400}}]})</li>
</ul>



<h2 class="wp-block-heading">#11 – Using $in &amp; $nin</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.find({ rating: {$in: [7,8,9]}})</li>



<li>db.books.find({$or: [{rating: 7}, {rating: 8}, {rating: 9}]})</li>



<li>db.books.find({rating: {$nin: [7,8,9]}})</li>



<li>db.books.find({rating: {$nin: [7,8]}})</li>
</ul>



<h2 class="wp-block-heading">#12 – Querying Arrays</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.find({genres: “fantasy”})</li>



<li>db.books.find({genres: “magic”})</li>



<li>db.books.find({genres: [“magic”]})</li>



<li>db.books.find({genres: [“fantasy”]})</li>



<li>db.books.find({genres: [“fantasy”, “magic”]})</li>



<li>db.books.find({genres: {$all: [“fantasy”, “sci-fi”]}})</li>



<li>db.books.find({“reviews.name”: “luigi”})</li>
</ul>



<h2 class="wp-block-heading">#13 – Deleting Documents</h2>



<p>事先使用 MongoDB Compass EXPORT DATA 把資料匯出成JSON檔案。</p>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.find()</li>



<li>db.books.deleteOne({_id: ObjectId(“650d22be248ed3d1c20642d0”)})</li>



<li>db.books.deleteMany({author: “Terry Pratchett”})</li>
</ul>



<p>使用 MongoDB Compass ADD DATA 把檔案匯入。</p>



<h2 class="wp-block-heading">#14 – Updating Documents</h2>



<h3 class="wp-block-heading">指令</h3>



<ul class="wp-block-list">
<li>db.books.updateOne({_id: ObjectId(“650d22be248ed3d1c20642cf”)}, {$set: {rating: 8, pages: 360}})</li>



<li>db.books.updateMany({author: “Terry Pratchett”}, {$set: {author: “Terry Pratchet”}})</li>



<li>db.books.updateOne({_id: ObjectId(“650d22be248ed3d1c20642d0”)}, {$inc: {pages: 2}})</li>



<li>db.books.updateOne({_id: ObjectId(“650d22be248ed3d1c20642d0”)}, {$inc: {pages: -2}})</li>



<li>db.books.updateOne({_id: ObjectId(“650d22be248ed3d1c20642d0”)}, {$pull: {genres: “fantasy”}})</li>



<li>db.books.updateOne({_id: ObjectId(“650d22be248ed3d1c20642d0”)}, {$push: {genres: “fantasy”}})</li>



<li>db.books.updateOne({_id: ObjectId(“650d22be248ed3d1c20642d0”)}, {$push: {genres: {$each: [“1″,”2”]}}})</li>
</ul>



<h2 class="wp-block-heading">#15 – MongoDB Drivers</h2>



<ul class="wp-block-list">
<li><a href="https://www.mongodb.com/docs/drivers/" target="_blank" rel="noreferrer noopener">Start Developing with MongoDB</a></li>



<li><a href="https://www.mongodb.com/docs/drivers/node/current/" target="_blank" rel="noreferrer noopener">MongoDB Node Driver</a></li>



<li><a href="https://nodejs.org/en" target="_blank" rel="noreferrer noopener">Node.js</a></li>
</ul>



<h3 class="wp-block-heading">指令 &amp; 操作步驟</h3>



<ol class="wp-block-list">
<li>npm init</li>



<li>建立一個 app.js 檔案</li>



<li>npm install express –save</li>



<li>npm install -g nodemon</li>



<li>nodemon app</li>



<li>http://localhost:3000/books</li>



<li>新增終端 npm install mongodb –save</li>
</ol>



<pre class="wp-block-code"><code>// app.js
const express = require('express')

// init app &amp; middleware
const app = express()

app.listen(3000, () =&gt; {
  console.log('app listening on port 3000')
})

// routes
app.get('/books', (req, res) =&gt; {
  res.json({mssg: "welcome to the api"})
})</code></pre>



<h2 class="wp-block-heading">#16 – Connecting to MongoDB</h2>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>建立一個 db.js 檔案</li>



<li>修改 app.js 檔案內容</li>
</ol>



<pre class="wp-block-code"><code>// db.js
const { MongoClient } = require('mongodb')

let dbConnection

module.exports = {
  connectToDb: (cb) =&gt; {
    MongoClient.connect('mongodb://localhost:27017/bookstore')
      .then((client) =&gt; {
        dbConnection = client.db()
        return cb()
      })
      .catch(err =&gt; {
        console.log(err)
        return cb(err)
      })
  },
  getDb: () =&gt; dbConnection
}</code></pre>



<pre class="wp-block-code"><code>// app.js
const express = require('express')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  res.json({mssg: "welcome to the api"})
})</code></pre>



<h2 class="wp-block-heading">#17 – Cursors &amp; Fetching Data</h2>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>繼續修改 app.js 檔案內容</li>
</ol>



<pre class="wp-block-code"><code>// app.js
const express = require('express')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})</code></pre>



<h2 class="wp-block-heading">#18 – Finding Single Documents</h2>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>繼續修改 app.js</li>



<li>使用 URL 測試是否能正常找到單一文件 http://localhost:3000/books/650d22be248ed3d1c20642cf</li>



<li>增加一些 ObjectId 布林判斷 app.js</li>
</ol>



<pre class="wp-block-code"><code>// app.js - 1
const express = require('express')
const { ObjectId } = require('mongodb')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})

app.get('/books/:id', (req, res) =&gt; {

  db.collection('books')
    .findOne({_id: new ObjectId(req.params.id)})
    .then(doc =&gt; {
      res.status(200).json(doc)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not fetch the document'})
    })

})
</code></pre>



<pre class="wp-block-code"><code>// app.js -3
const express = require('express')
const { ObjectId } = require('mongodb')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})

app.get('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .findOne({_id: new ObjectId(req.params.id)})
    .then(doc =&gt; {
      res.status(200).json(doc)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not fetch the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
  
})</code></pre>



<h2 class="wp-block-heading">#19 – Using POSTMAN</h2>



<ul class="wp-block-list">
<li><a href="https://www.postman.com/" target="_blank" rel="noreferrer noopener">POSTMAN</a></li>



<li><a href="https://hoppscotch.io/" target="_blank" rel="noreferrer noopener">HOPPSCOTCH</a></li>



<li><a rel="noreferrer noopener" href="https://docs.hoppscotch.io/documentation/features/interceptor" target="_blank">HOPPSCOTCH Browser extension</a></li>
</ul>



<h3 class="wp-block-heading">Request</h3>



<ul class="wp-block-list">
<li>http://localhost:3000/books</li>



<li>http://localhost:3000/books/650d1421248ed3d1c20642cc</li>
</ul>



<h2 class="wp-block-heading">#20 – Handling POST Request</h2>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>在 app.js 新增 post 相關程式碼</li>



<li>使用 POSTMAN 請求 POST 方法</li>



<li>http://localhost:3000/books</li>



<li>Body &gt; raw 貼上準備好的資料內容然後送出</li>



<li>可以看到 Response Body 呈現的訊息</li>



<li>點擊之前的 Get Collection – books 測試</li>
</ol>



<pre class="wp-block-code"><code>// app.js
const express = require('express')
const { ObjectId } = require('mongodb')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()
app.use(express.json())

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})

app.get('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .findOne({_id: new ObjectId(req.params.id)})
    .then(doc =&gt; {
      res.status(200).json(doc)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not fetch the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
  
})

app.post('/books', (req, res) =&gt; {
  const book = req.body

  db.collection('books')
    .insertOne(book)
    .then(result =&gt; {
      res.status(201).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({err: 'Could not create a new document'})
    })
})</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw JSON
{
  "title": "The Final Empire",
  "author": "Brandon Sanderson",
  "rating": 9,
  "pages": 420,
  "genres": &#91;
    "fantasy",
    "magic"
  ],
  "reviews": &#91;
    {
      "name": "Shaun",
      "body": "Couldn't put this book down."
    },
    {
      "name": "Chun-Li",
      "body": "Love it."
    }
  ]
}
</code></pre>



<h2 class="wp-block-heading">#21 – Handling DELETE Requests</h2>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>在 app.js 新增 delete 相關程式碼</li>



<li>使用 POSTMAN 請求 DELETE 方法</li>



<li>http://localhost:3000/books/650d22be248ed3d1c20642d0</li>



<li>送出後可以看到 Response Body 的訊息</li>



<li>點擊 Get 方法查看是否有刪除資料</li>
</ol>



<pre class="wp-block-code"><code>// app.js
const express = require('express')
const { ObjectId } = require('mongodb')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()
app.use(express.json())

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})

app.get('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .findOne({_id: new ObjectId(req.params.id)})
    .then(doc =&gt; {
      res.status(200).json(doc)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not fetch the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
  
})

app.post('/books', (req, res) =&gt; {
  const book = req.body

  db.collection('books')
    .insertOne(book)
    .then(result =&gt; {
      res.status(201).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({err: 'Could not create a new document'})
    })
})

app.delete('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .deleteOne({_id: new ObjectId(req.params.id)})
    .then(result =&gt; {
      res.status(200).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not delete the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
})</code></pre>



<h2 class="wp-block-heading">#22 – PATCH Requests</h2>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>在 app.js 新增 PATCH 相關程式碼</li>



<li>使用 POSTMAN 請求 PATCH 方法</li>



<li>因為出現錯誤，有重新 import bookstore.books 檔案</li>



<li>http://localhost:3000/books/650d1421248ed3d1c20642cc</li>



<li>Body &gt; raw JSON 更新資料後送出會呈現 Body 訊息</li>



<li>使用 GET Collection 查看是否有正確更新</li>
</ol>



<pre class="wp-block-code"><code>// app.js
const express = require('express')
const { ObjectId } = require('mongodb')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()
app.use(express.json())

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})

app.get('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .findOne({_id: new ObjectId(req.params.id)})
    .then(doc =&gt; {
      res.status(200).json(doc)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not fetch the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
  
})

app.post('/books', (req, res) =&gt; {
  const book = req.body

  db.collection('books')
    .insertOne(book)
    .then(result =&gt; {
      res.status(201).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({err: 'Could not create a new document'})
    })
})

app.delete('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .deleteOne({_id: new ObjectId(req.params.id)})
    .then(result =&gt; {
      res.status(200).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not delete the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
})

app.patch('/books/:id', (req, res) =&gt; {
  const updates = req.body

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .updateOne({_id: new ObjectId(req.params.id)}, {$set: updates})
    .then(result =&gt; {
      res.status(200).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not update the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
})</code></pre>



<pre class="wp-block-code"><code>Body &gt; raw JSON
{
  "pages": 350,
  "rating": 8
}</code></pre>



<h2 class="wp-block-heading">#23 – Pagination</h2>



<p>In this MongoDB tutorial I’ll explain how to implement pagination into your requests.</p>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>在 app.js 新增關於 pagination 程式碼</li>



<li>使用 POSTMAN 請求 GET 方法<br>以下兩種結果會相同</li>



<li>http://localhost:3000/books</li>



<li>http://localhost:3000/books?p=0</li>



<li>使用 http://localhost:3000/books?p=1<br>會忽略前面的資料</li>



<li>http://localhost:3000/books?p=2<br>因為沒有資料會出現空陣列</li>
</ol>



<pre class="wp-block-code"><code>// app.js
const express = require('express')
const { ObjectId } = require('mongodb')
const { connectToDb, getDb } = require('./db')

// init app &amp; middleware
const app = express()
app.use(express.json())

// db connection
let db

connectToDb((err) =&gt; {
  if (!err) {
    app.listen(3000, () =&gt; {
      console.log('app listening on port 3000')
    })
    db = getDb()
  }
})

// routes
app.get('/books', (req, res) =&gt; {
  // current page
  const page = req.query.p || 0
  const booksPerPage = 3

  let books = &#91;]

  db.collection('books')
    .find()
    .sort({ author: 1 })
    .skip(page * booksPerPage)
    .limit(booksPerPage)
    .forEach(book =&gt; books.push(book))
    .then(() =&gt; {
      res.status(200).json(books)
    })
    .catch(() =&gt; {
      res.status(500).json({error: 'Could not fetch the documents'})
    })
})

app.get('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .findOne({_id: new ObjectId(req.params.id)})
    .then(doc =&gt; {
      res.status(200).json(doc)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not fetch the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
  
})

app.post('/books', (req, res) =&gt; {
  const book = req.body

  db.collection('books')
    .insertOne(book)
    .then(result =&gt; {
      res.status(201).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({err: 'Could not create a new document'})
    })
})

app.delete('/books/:id', (req, res) =&gt; {

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .deleteOne({_id: new ObjectId(req.params.id)})
    .then(result =&gt; {
      res.status(200).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not delete the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
})

app.patch('/books/:id', (req, res) =&gt; {
  const updates = req.body

  if (ObjectId.isValid(req.params.id)) {
    db.collection('books')
    .updateOne({_id: new ObjectId(req.params.id)}, {$set: updates})
    .then(result =&gt; {
      res.status(200).json(result)
    })
    .catch(err =&gt; {
      res.status(500).json({error: 'Could not update the document'})
    })
  } else {
    res.status(500).json({error: 'Not a valid doc id'})
  }
})
</code></pre>



<h2 class="wp-block-heading">#24 – Indexes</h2>



<p>In this mongodb tutorial we’ll talk about indexes and see how to create them.</p>



<h3 class="wp-block-heading">簡報</h3>



<ul class="wp-block-list">
<li>db.collection(“books”).find({“rating”: 10})</li>
</ul>



<h3 class="wp-block-heading">指令 &amp; 操作步驟</h3>



<ol class="wp-block-list">
<li>使用終端機執行 mongosh</li>



<li>使用 bookstore 資料庫 use bookstore</li>



<li>db.books.find({rating: 8}).explain(‘executionStats’)</li>



<li>db.books.createIndex({ rating: 8 })</li>



<li>db.books.getIndexes()</li>



<li>db.books.dropIndex({rating: 8})</li>



<li>db.books.getIndexes()</li>
</ol>



<h2 class="wp-block-heading">#25 – MongoDB Atlas</h2>



<p>In this mongodb tutorial you’ll learn how to use MongoDB Atlas, a Cloud Database service which allows you to easily set up a hosted database online.</p>



<h3 class="wp-block-heading">操作步驟</h3>



<ol class="wp-block-list">
<li>註冊或登入 MongoDB</li>



<li>DEPLOYMENT &gt; Database 選擇建立一個 Database</li>



<li>選擇 Shared 免費方案</li>



<li>aws &gt; default 或者自行選擇 &gt; Cluster Name 默認原本名稱 &gt; 建立 Cluster</li>



<li>Security Quickstart 可以設定 Username 和 Password</li>



<li>設定好後可以到 Database Access 查看</li>



<li>Network Access &gt; 新增一個 IP address &gt; 在這裡選擇 ALLOW ACCESS FROM ANYWHERE 僅作測試使用，之後生產版本請勿選擇這個選項</li>



<li>到 Database Deployment 點擊 Connection &gt; 選擇 Connect your application</li>



<li>Add your connection string into your application code</li>



<li>在 db.js 修改成指定的 connection string &gt; 修改設定的 Username 和 Password</li>



<li>我們不需要修改這些 Request，我們仍然使用這些 Request 關於 localhost</li>



<li>Mongodb online 資料庫尚未有任何資料 &gt; 可以 post 新的資料到線上資料庫 &gt; Browse Collections</li>
</ol>



<pre class="wp-block-code"><code>// db.js
const { MongoClient } = require('mongodb')

let dbConnection
let uri = '指定的 connection string'

module.exports = {
  connectToDb: (cb) =&gt; {
    MongoClient.connect(uri)
      .then((client) =&gt; {
        dbConnection = client.db()
        return cb()
      })
      .catch(err =&gt; {
        console.log(err)
        return cb(err)
      })
  },
  getDb: () =&gt; dbConnection
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Build Web Apps With Vue JS 3 &#038; Firebase (1)</title>
		<link>/wordpress_blog/vuejs3firebase-1/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 16 Feb 2022 07:01:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=536</guid>

					<description><![CDATA[Learn Vue JS 3 &#38; Firebase by [&#8230;]]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Learn Vue JS 3 &amp; Firebase by creating &amp; deploying dynamic web apps (including Authentication).</p><cite>建立者: The Net Ninja (Shaun Pelling)</cite></blockquote>



<h3 class="wp-block-heading">您會學到</h3>



<ul class="wp-block-list"><li>Learn how to create Vue 3 applications from the ground-up</li><li>Get in-depth knowledge of Vue features like the Vue Router, Vue CLI, Options API, Composition API, Teleport etc</li><li>Get hands-on &amp; in-depth experience using the latest Vue 3 features (such as the Composition API)</li><li>Learn how to use Firebase as a back-end to your Vue applications (as a database, authentication service, hosting etc)</li><li>Build &amp; deploy 4 real-word web apps with Vue &amp; Firebase</li><li>Learn how to implement an authentication system into your Vue js apps using Firebase Auth</li><li>Become a Vue 3 ninja!</li></ul>



<h2 class="wp-block-heading">第1節：Introduction &amp; Setup</h2>



<h3 class="wp-block-heading">What is Vue?</h3>



<h4 class="wp-block-heading">What is Vue?</h4>



<ul class="wp-block-list"><li>Front-end, JavaScript / TypeScript framework</li><li>Used to create dynamic &amp; data-driven websites (SPA’s)</li><li>Can also be used to create stand-alone widgets</li></ul>



<h4 class="wp-block-heading">Vue Widgets</h4>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="1536" height="823" src="/wordpress_blog/wp-content/uploads/2022/04/Vue-Widgets.png" alt="" class="wp-image-538"/><figcaption>Vue Widgets</figcaption></figure>



<h4 class="wp-block-heading">Vue Websites</h4>



<ul class="wp-block-list"><li>Vue is used to create a whole website with multiple pages &amp; components</li><li>These websites are normally called Single Page Applications</li><li>All routing is done in the browser &amp; not on the server</li></ul>



<h4 class="wp-block-heading">Typical non-Vue Websites</h4>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img decoding="async" width="1755" height="603" data-id="545" src="/wordpress_blog/wp-content/uploads/2022/04/Typical-non-Vue-Websites-01.png" alt="" class="wp-image-545"/><figcaption>Typical non-Vue Website 01</figcaption></figure>



<figure class="wp-block-image size-large"><img decoding="async" width="1765" height="615" data-id="546" src="/wordpress_blog/wp-content/uploads/2022/04/Typical-non-Vue-Websites-02.png" alt="" class="wp-image-546"/><figcaption>Typical non-Vue Website 02</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1743" height="579" data-id="547" src="/wordpress_blog/wp-content/uploads/2022/04/Typical-non-Vue-Websites-03.png" alt="" class="wp-image-547"/><figcaption>Typical non-Vue Website 03</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1769" height="605" data-id="548" src="/wordpress_blog/wp-content/uploads/2022/04/Typical-non-Vue-Websites-04.png" alt="" class="wp-image-548"/><figcaption>Typical non-Vue Website 04</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1759" height="705" data-id="549" src="/wordpress_blog/wp-content/uploads/2022/04/Typical-non-Vue-Websites-05.png" alt="" class="wp-image-549"/><figcaption>Typical non-Vue Website 05</figcaption></figure>
</figure>



<h4 class="wp-block-heading">Vue Websites</h4>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-2 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1747" height="609" data-id="554" src="/wordpress_blog/wp-content/uploads/2022/04/Vue-Websites-01.png" alt="" class="wp-image-554"/><figcaption>Vue Websites 01</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1743" height="595" data-id="552" src="/wordpress_blog/wp-content/uploads/2022/04/Vue-Websites-02.png" alt="" class="wp-image-552"/><figcaption>Vue Websites 02</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1741" height="571" data-id="553" src="/wordpress_blog/wp-content/uploads/2022/04/Vue-Websites-03.png" alt="" class="wp-image-553"/><figcaption>Vue Websites 03</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1747" height="593" data-id="551" src="/wordpress_blog/wp-content/uploads/2022/04/Vue-Websites-04.png" alt="" class="wp-image-551"/><figcaption>Vue Websites 04</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1745" height="597" data-id="555" src="/wordpress_blog/wp-content/uploads/2022/04/Vue-Websites-05.png" alt="" class="wp-image-555"/><figcaption>Vue Websites 05</figcaption></figure>
</figure>



<h4 class="wp-block-heading">Single Page Application (SPA)</h4>



<ul class="wp-block-list"><li>Only a single HTML page sent (initially) to the browser</li><li>Vue intercepts subsequent requests and handles “page” changes in the browser by swapping what components are shown on the page</li><li>Results in a much faster and smoother website experience</li></ul>



<h3 class="wp-block-heading">New Features in Vue 3</h3>



<h4 class="wp-block-heading">Vue 3 New Features</h4>



<h4 class="wp-block-heading">The Composition API</h4>



<ul class="wp-block-list"><li>Improves on reusablility, organization &amp; readability</li><li>It does this by giving us a new setup( ) function</li></ul>



<h4 class="wp-block-heading">Multiple Root Elements</h4>



<ul class="wp-block-list"><li>Can have many root elements side-by-side in a component</li></ul>



<pre class="wp-block-code"><code>&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;Hello, World!&lt;/p&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;p&gt;Hello, again!&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>



<h4 class="wp-block-heading">Teleport Component</h4>



<ul class="wp-block-list"><li>Render content from one component in a different place in the DOM</li><li>Useful for things like modals</li></ul>



<h4 class="wp-block-heading">Suspense Component</h4>



<ul class="wp-block-list"><li>Used to handle asynchronous components easily</li><li>Can provide fall-back content (e.g. a spinner) until data is loaded</li></ul>



<h4 class="wp-block-heading">TypeScript Support</h4>



<ul class="wp-block-list"><li>Can now write Vue applications using TypeScript</li></ul>



<h4 class="wp-block-heading">More Changes</h4>



<ul class="wp-block-list"><li>Multiple v-models for custom components</li><li>Improved Reactivity</li><li>Performance gains</li></ul>



<h3 class="wp-block-heading">What You Should Already Know</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://www.youtube.com/c/TheNetNinja" target="_blank">Net Ninja Youtube Channel (more free tutorials)</a></li><li><a rel="noreferrer noopener" href="https://www.udemy.com/course/modern-javascript-from-novice-to-ninja/" target="_blank">Modern JavaScript Course</a></li><li><a href="https://www.youtube.com/watch?v=hu-q2zYwEYs&amp;list=PL4cUxeGkcC9ivBf_eKCPIAYXWzLlPAm6G" target="_blank" rel="noreferrer noopener">HTML &amp; CSS Free Crash Course</a></li></ul>



<h4 class="wp-block-heading">Before You Start…</h4>



<ul class="wp-block-list"><li>Understand the foundations of JavaScript<ul><li>functions, objects, arrays, etc</li></ul></li><li>HTML &amp; (some) CSS</li></ul>



<h3 class="wp-block-heading">Environment Setup</h3>



<ul class="wp-block-list"><li>網頁編輯器：VSCode</li><li>擴充套件(Extensions)：Live Server、Vetur、Material Icon Theme</li></ul>



<h3 class="wp-block-heading">Course Files</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://github.com/iamshaunjp/Vue-3-Firebase" target="_blank">GitHub Course Files</a></li><li><a href="https://github.com/iamshaunjp/Vue-3-Firebase/blob/Installing-Dependencies/README.md" target="_blank" rel="noreferrer noopener">Guide for using the course files (for later lessons)</a></li></ul>



<h2 class="wp-block-heading">Vue Basics</h2>



<h3 class="wp-block-heading">How to use Vue (using the CDN)</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://v3.vuejs.org/guide/introduction.html" target="_blank" rel="noreferrer noopener">Vue Documentation</a></li></ul>



<h4 class="wp-block-heading">Vue CDN</h4>



<ul class="wp-block-list"><li>使用以下版本</li></ul>



<pre class="wp-block-code"><code>&lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;</code></pre>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>&lt;script&gt; 載入 Vue CDN vue@3.0.2 版本</li><li>新增 app.js 檔案</li><li>&lt;script&gt; 載入 app.js</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

console.log('hello, vue')</code></pre>



<h3 class="wp-block-heading">Creating a Vue App</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;h2&gt;I am the template now&lt;/h2&gt;
  &lt;/div&gt;


  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  // data, functions
  // template: '&lt;h2&gt;I am the template&lt;/h2&gt;'
})

app.mount('#app')</code></pre>



<h3 class="wp-block-heading">Templates &amp; Data</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;p&gt;{{ title }} - {{ author }} - {{ age }}&lt;/p&gt;
  &lt;/div&gt;


  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      title: 'The Final Empire',
      author: 'Brandon Sanderson',
      age: 45
    }
  }
})

app.mount('#app')</code></pre>



<h3 class="wp-block-heading">Methods &amp; Click Events</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;p&gt;{{ title }} - {{ author }} - {{ age }}&lt;/p&gt;

    &lt;button v-on:click="age++"&gt;Increase age&lt;/button&gt;
    &lt;button v-on:click="age--"&gt;Decrease age&lt;/button&gt;
    &lt;div @click="changeTitle('Oathbringer')"&gt;Change book title&lt;/div&gt;
  &lt;/div&gt;


  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      title: 'The Final Empire',
      author: 'Brandon Sanderson',
      age: 45
    }
  },
  methods: {
    changeTitle(title) {
      // this.title = 'Words of Randiance'
      this.title = title
    }
  }
})

app.mount('#app')</code></pre>



<h3 class="wp-block-heading">Conditional Rendering (條件渲染)</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;div v-if="showBooks"&gt;
      &lt;p&gt;{{ title }} - {{ author }} - {{ age }}&lt;/p&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;

    &lt;div v-show="showBooks"&gt;currently showing books&lt;/div&gt;
  &lt;/div&gt;


  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      showBooks: true,
      title: 'The Final Empire',
      author: 'Brandon Sanderson',
      age: 45
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    }
  }
})

app.mount('#app')</code></pre>



<h3 class="wp-block-heading">Other Mouse Events</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    .box {
      padding: 100px 0;
      width: 400px;
      text-align: center;
      background: #ddd;
      margin: 20px;
      display: inline-block;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;div v-if="showBooks"&gt;
      &lt;p&gt;{{ title }} - {{ author }} - {{ age }}&lt;/p&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;

    &lt;br&gt;
    &lt;!-- mouse events --&gt;
    &lt;div class="box" @mouseover="handleEvent($event, 5)"&gt;mouseover (enter)&lt;/div&gt;
    &lt;div class="box" @mouseleave="handleEvent"&gt;mouseleave&lt;/div&gt;
    &lt;div class="box" @dblclick="handleEvent"&gt;double click&lt;/div&gt;
    &lt;div class="box" @mousemove="handleMousemove"&gt;position - {{ x }} {{ y }}&lt;/div&gt;
    
  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      showBooks: true,
      title: 'The Final Empire',
      author: 'Brandon Sanderson',
      age: 45,
      x: 0,
      y: 0
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
    handleEvent(e, data) {
      console.log(e, e.type)
      if (data) {
        console.log(data)
      }
    },
    handleMousemove(e) {
      this.x = e.offsetX
      this.y = e.offsetY
    }
  }
})

app.mount('#app')</code></pre>



<h3 class="wp-block-heading">Outputting Lists (v-for)</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    .box {
      padding: 100px 0;
      width: 400px;
      text-align: center;
      background: #ddd;
      margin: 20px;
      display: inline-block;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;div v-if="showBooks"&gt;
      &lt;ul&gt;
        &lt;li v-for="book in books"&gt;
          &lt;h3&gt;{{ book.title }}&lt;/h3&gt;
          &lt;p&gt;{{ book.author }}&lt;/p&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;

    
  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      showBooks: true,
      books: &#91;
        { title: 'name of the wind', author: 'patrick rothfuss'},
        { title: 'the way of kings', author: 'brandon sanderson'},
        { title: 'the final empire', author: 'brandon sanderson'},
      ]
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
  }
})

app.mount('#app')</code></pre>



<h3 class="wp-block-heading">Attribute Binding</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    .box {
      padding: 100px 0;
      width: 400px;
      text-align: center;
      background: #ddd;
      margin: 20px;
      display: inline-block;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;!-- attribute binding --&gt;
    &lt;!-- &lt;a :href="url"&gt;Best website ever&lt;/a&gt; --&gt;

    &lt;div v-if="showBooks"&gt;
      &lt;ul&gt;
        &lt;li v-for="book in books"&gt;
          &lt;img :src="book.img" :alt="book.title"&gt;
          &lt;h3&gt;{{ book.title }}&lt;/h3&gt;
          &lt;p&gt;{{ book.author }}&lt;/p&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;

    
  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      url: 'http://www.thenetninja.co.uk/',
      showBooks: true,
      books: &#91;
        { title: 'name of the wind', author: 'patrick rothfuss', img: 'assets/1.jpg'},
        { title: 'the way of kings', author: 'brandon sanderson', img: 'assets/2.jpg'},
        { title: 'the final empire', author: 'brandon sanderson', img: 'assets/3.jpg'},
      ]
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
  }
})

app.mount('#app')</code></pre>



<h4 class="wp-block-heading">縮寫</h4>



<pre class="wp-block-code"><code>v-on = @
v-bind = :</code></pre>



<h3 class="wp-block-heading">Dynamic Classes</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://www.youtube.com/watch?v=Y8zMYaD1bz0&amp;list=PL4cUxeGkcC9i3FXJSUfmsNOx8E7u6UuhG" target="_blank" rel="noreferrer noopener">Free Flexbox Tutorial</a></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    body {
      background: #eee;
      max-width: 960px;
      margin: 20px auto;
    }
    p, h3, ul {
      margin: 0;
      padding: 0;
    }
    li {
      list-style-type: none;
      background: #fff;
      margin: 20px auto;
      padding: 10px 20px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    li.fav {
      background: #ff9ed2;
      color: #fff;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;!-- attribute binding --&gt;
    &lt;!-- &lt;a :href="url"&gt;Best website ever&lt;/a&gt; --&gt;

    &lt;div v-if="showBooks"&gt;
      &lt;ul&gt;
        &lt;li v-for="book in books" :class="{ fav: book.isFav }"&gt;
          &lt;img :src="book.img" :alt="book.title"&gt;
          &lt;h3&gt;{{ book.title }}&lt;/h3&gt;
          &lt;p&gt;{{ book.author }}&lt;/p&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;

  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      url: 'http://www.thenetninja.co.uk/',
      showBooks: true,
      books: &#91;
        { title: 'name of the wind', author: 'patrick rothfuss', img: 'assets/1.jpg', isFav: true },
        { title: 'the way of kings', author: 'brandon sanderson', img: 'assets/2.jpg', isFav: false },
        { title: 'the final empire', author: 'brandon sanderson', img: 'assets/3.jpg', isFav: true },
      ]
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
  }
})

app.mount('#app')</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">CHALLENGE – Add to Favs</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    body {
      background: #eee;
      max-width: 960px;
      margin: 20px auto;
    }
    p, h3, ul {
      margin: 0;
      padding: 0;
    }
    li {
      list-style-type: none;
      background: #fff;
      margin: 20px auto;
      padding: 10px 20px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    li.fav {
      background: #ff9ed2;
      color: #fff;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;!-- attribute binding --&gt;
    &lt;!-- &lt;a :href="url"&gt;Best website ever&lt;/a&gt; --&gt;

    &lt;div v-if="showBooks"&gt;
      &lt;ul&gt;
        &lt;li v-for="book in books" :class="{ fav: book.isFav }"&gt;
          &lt;img :src="book.img" :alt="book.title"&gt;
          &lt;h3&gt;{{ book.title }}&lt;/h3&gt;
          &lt;p&gt;{{ book.author }}&lt;/p&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;

    
  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      url: 'http://www.thenetninja.co.uk/',
      showBooks: true,
      books: &#91;
        { title: 'name of the wind', author: 'patrick rothfuss', img: 'assets/1.jpg', isFav: true },
        { title: 'the way of kings', author: 'brandon sanderson', img: 'assets/2.jpg', isFav: false },
        { title: 'the final empire', author: 'brandon sanderson', img: 'assets/3.jpg', isFav: true },
      ]
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
  }
})

app.mount('#app')

// Challenge - Add to Favs
// - attach a click event to each li tag (for each book)
// - when a user clicks an li, toggle the 'isFav' property of that book</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">SOLUTION – Add to Favs</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    body {
      background: #eee;
      max-width: 960px;
      margin: 20px auto;
    }
    p, h3, ul {
      margin: 0;
      padding: 0;
    }
    li {
      list-style-type: none;
      background: #fff;
      margin: 20px auto;
      padding: 10px 20px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    li.fav {
      background: #ff9ed2;
      color: #fff;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;!-- attribute binding --&gt;
    &lt;!-- &lt;a :href="url"&gt;Best website ever&lt;/a&gt; --&gt;

    &lt;div v-if="showBooks"&gt;
      &lt;ul&gt;
        &lt;li v-for="book in books" :class="{ fav: book.isFav }" @click="toggleFav(book)"&gt;
          &lt;img :src="book.img" :alt="book.title"&gt;
          &lt;h3&gt;{{ book.title }}&lt;/h3&gt;
          &lt;p&gt;{{ book.author }}&lt;/p&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;
    
  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      url: 'http://www.thenetninja.co.uk/',
      showBooks: true,
      books: &#91;
        { title: 'name of the wind', author: 'patrick rothfuss', img: 'assets/1.jpg', isFav: true },
        { title: 'the way of kings', author: 'brandon sanderson', img: 'assets/2.jpg', isFav: false },
        { title: 'the final empire', author: 'brandon sanderson', img: 'assets/3.jpg', isFav: true },
      ]
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
    toggleFav(book) {
      book.isFav = !book.isFav
    }
  }
})

app.mount('#app')

// Challenge - Add to Favs
// - attach a click event to each li tag (for each book)
// - when a user clicks an li, toggle the 'isFav' property of that book</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Computes Properties</h3>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Learning Vue&lt;/title&gt;
  &lt;script src="https://unpkg.com/vue@3.0.2"&gt;&lt;/script&gt;
  &lt;style&gt;
    body {
      background: #eee;
      max-width: 960px;
      margin: 20px auto;
    }
    p, h3, ul {
      margin: 0;
      padding: 0;
    }
    li {
      list-style-type: none;
      background: #fff;
      margin: 20px auto;
      padding: 10px 20px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    li.fav {
      background: #ff9ed2;
      color: #fff;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Hello, Vue :)&lt;/h1&gt;

  &lt;div id="app"&gt;
    &lt;!-- attribute binding --&gt;
    &lt;!-- &lt;a :href="url"&gt;Best website ever&lt;/a&gt; --&gt;

    &lt;div v-if="showBooks"&gt;
      &lt;ul&gt;
        &lt;li v-for="book in filteredBooks" :class="{ fav: book.isFav }" @click="toggleFav(book)"&gt;
          &lt;img :src="book.img" :alt="book.title"&gt;
          &lt;h3&gt;{{ book.title }}&lt;/h3&gt;
          &lt;p&gt;{{ book.author }}&lt;/p&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button @click="toggleShowBooks"&gt;
      &lt;span v-if="showBooks"&gt;Hide Books&lt;/span&gt;
      &lt;span v-else&gt;Show Books&lt;/span&gt;
    &lt;/button&gt;
    
  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const app = Vue.createApp({
  data() {
    return {
      url: 'http://www.thenetninja.co.uk/',
      showBooks: true,
      books: &#91;
        { title: 'name of the wind', author: 'patrick rothfuss', img: 'assets/1.jpg', isFav: true },
        { title: 'the way of kings', author: 'brandon sanderson', img: 'assets/2.jpg', isFav: false },
        { title: 'the final empire', author: 'brandon sanderson', img: 'assets/3.jpg', isFav: true },
      ]
    }
  },
  methods: {
    toggleShowBooks() {
      this.showBooks = !this.showBooks
    },
    toggleFav(book) {
      book.isFav = !book.isFav
    }
  },
  computed: {
    filteredBooks() {
      return this.books.filter((book) =&gt; book.isFav)
    }
  }
})

app.mount('#app')
</code></pre>



<h2 class="wp-block-heading">第3節：The Vue CLI (for Bigger Projects)</h2>



<h3 class="wp-block-heading">Why Use the Vue CLI?</h3>



<h4 class="wp-block-heading">Vue Websites</h4>



<ul class="wp-block-list"><li>Use modern JavaScript features</li><li>Provides us with a live-reload dev server</li><li>Optimize our code for production</li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">How to Use the Vue CLI</h3>



<ul class="wp-block-list"><li>Node.js – 必要</li><li>Command line – 必要，Node.js command prompt、終端機、命令提示字元</li><li>Node.js 版本查詢 node -v</li><li>Vue CLI，npm install -g @vue/cli</li><li>移動到專案位置，cd 專案名稱位置</li><li>執行指令 vue create modal-project</li></ul>



<h4 class="wp-block-heading">關於舊版本</h4>



<p>Vue CLI 的包名稱由&nbsp;<code>vue-cli</code>&nbsp;改成了<code>@vue/cli</code>。如果你已經全局安裝了舊版本的<code>vue-cli</code>(1.x 或2.x)，你需要先通過&nbsp;<code>npm uninstall vue-cli -g</code>&nbsp;或&nbsp;<code>yarn global remove vue-cli</code>&nbsp;卸載它。</p>



<ul class="wp-block-list"><li><a href="https://cli.vuejs.org/zh/guide/installation.html" target="_blank" rel="noreferrer noopener">Vue CLI 安裝連結</a></li></ul>



<h4 class="wp-block-heading">執行指令步驟 vue create modal-project</h4>



<ul class="wp-block-list"><li>Manually select features</li><li>Check the features needed for your project:<ul><li>Choose Vue version</li><li>Babel</li></ul></li><li>Choose a version of Vue.js that you want to start the project with<ul><li>3.x</li></ul></li><li>Where do you prefer placing config for Babel, ESLint, etc.?<ul><li>In dedicated config files</li></ul></li><li>Save this as a preset for future projects<ul><li>N</li></ul></li></ul>



<h4 class="wp-block-heading">安裝完成後指令</h4>



<ul class="wp-block-list"><li>cd 專案名稱</li><li>使用 code . 打開 VSCode 編輯器</li></ul>



<h3 class="wp-block-heading">New Project Walkthrough</h3>



<h3 class="wp-block-heading">Vue Files &amp; Templates</h3>



<h4 class="wp-block-heading">打開 VSCode 終端機</h4>



<ul class="wp-block-list"><li>執行指令：npm run serve</li></ul>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: 'App',
  data() {
    return {
      title: 'My First Vue App :)'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<h4 class="wp-block-heading">下載 GitHub Course Files 須知</h4>



<ul class="wp-block-list"><li>安裝 node_modules</li><li>開啟 Terminal</li><li>執行指令 npm install</li></ul>



<h3 class="wp-block-heading">Template Refs (模板引用)</h3>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;input type="text" ref="name"&gt;
  &lt;button @click="handleClick"&gt;click me&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: 'App',
  data() {
    return {
      title: 'My First Vue App :)'
    }
  },
  methods: {
    handleClick() {
      console.log(this.$refs.name)
      this.$refs.name.classList.add('active')
      this.$refs.name.focus()
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<h3 class="wp-block-heading">Multiple Components</h3>



<h4 class="wp-block-heading">Multiple Components</h4>



<ul class="wp-block-list"><li>App.vue – root component<ul><li>Header.vue</li><li>Article.vue<ul><li>Content.vue</li><li>Comments.vue</li></ul></li><li>Footer.vue</li></ul></li></ul>



<h4 class="wp-block-heading">Terminology</h4>



<ul class="wp-block-list"><li>Article.vue – parent component<ul><li>Content.vue, Comments.vue – child components</li></ul></li><li>App.vue – parent component<ul><li>Header.vue, Article.vue, Footer.vue- child components</li></ul></li></ul>



<p>Component Tree，App.vue, Header.vue, Article.vue, Footer,vue, Content.vue, Comments.vue。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>移除 Components 裡面的 HelloWorld.vue</li><li>在 Components 裡面新增 Modal.vue</li></ul>



<pre class="wp-block-code"><code>// Modal.vue

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal"&gt;
      &lt;p&gt;modal content&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;Modal /&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<h3 class="wp-block-heading">Component Styles &amp; Global Styles</h3>



<h4 class="wp-block-heading">Modal.vue style 設定方式</h4>



<ul class="wp-block-list"><li>&lt;style&gt; 加上 scoped</li><li>make the selector more specific</li></ul>



<pre class="wp-block-code"><code>// Modal.vue - 1

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal"&gt;
      &lt;h1&gt;Modal Title&lt;/h1&gt;
      &lt;p&gt;modal content&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 2

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal"&gt;
      &lt;h1&gt;Modal Title&lt;/h1&gt;
      &lt;p&gt;modal content&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
&lt;/style&gt;</code></pre>



<h4 class="wp-block-heading">global styles</h4>



<ul class="wp-block-list"><li>新增 global.css 在 assets 資料夾裡面</li><li>設定進入點 main.js import 匯入 global.css 檔案</li><li>覆蓋 global.css 樣式，在 Modal.vue 修改樣式</li></ul>



<pre class="wp-block-code"><code>// assets/global.css

body {
  margin: 0;
}
p {
  font-style: italic;
}</code></pre>



<pre class="wp-block-code"><code>// src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import './assets/global.css'

createApp(App).mount('#app')
</code></pre>



<pre class="wp-block-code"><code>// components/Modal.vue

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal"&gt;
      &lt;h1&gt;Modal Title&lt;/h1&gt;
      &lt;p&gt;modal content&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Passing Data with Props (使用 Props 傳遞資料)</h3>



<h4 class="wp-block-heading">we do this by using what’s known as props in vue, and we can pass props from a parent component to a child component</h4>



<h4 class="wp-block-heading">為什麼我們要這樣做，兩個原因</h4>



<ul class="wp-block-list"><li>more dynamic and more reusable(可重複使用的)</li><li>multiple components and they all use the same data</li></ul>



<pre class="wp-block-code"><code>// App.vue - 1

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;Modal header="Sign up for the Giveway!" text="Grab your ninja swag for half price!" /&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 1

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal"&gt;
      &lt;h1&gt;{{ header }}&lt;/h1&gt;
      &lt;p&gt;{{ text }}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'header', 'text']
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue - 2

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;Modal :header="header" :text="text" /&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 2

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal"&gt;
      &lt;h1&gt;{{ header }}&lt;/h1&gt;
      &lt;p&gt;{{ text }}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'header', 'text']
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue - 3

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;Modal :header="header" :text="text" theme="sale" /&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 3

&lt;template&gt;
  &lt;div class="backdrop"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;h1&gt;{{ header }}&lt;/h1&gt;
      &lt;p&gt;{{ text }}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'header', 'text', 'theme']
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
&lt;/style&gt;</code></pre>



<p>And this is how we pass data as props into components, which A makes them more reusable, B makes them customizable, and C allows us to have a single source of truth when it comes to data.</p>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Emitting Custom Events</h3>



<p>show / hide Modal</p>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;div v-if="showModal"&gt;
    &lt;Modal :header="header" :text="text" theme="sale" @close="toggleModal" /&gt;
  &lt;/div&gt;
  &lt;button @click="toggleModal"&gt;open modal&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!',
      showModal: false
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Modal.vue

&lt;template&gt;
  &lt;div class="backdrop" @click="closeModal"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;h1&gt;{{ header }}&lt;/h1&gt;
      &lt;p&gt;{{ text }}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'header', 'text', 'theme'],
  methods: {
    closeModal() {
      this.$emit('close')
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
&lt;/style&gt;</code></pre>



<h3 class="wp-block-heading">Click Event Modifiers</h3>



<h4 class="wp-block-heading">open modal</h4>



<ul class="wp-block-list"><li>@click.right=”toggleModal”</li><li>@click.shift=”toggleModal”</li><li>@click.alt=”toggleModal”</li></ul>



<h4 class="wp-block-heading">close modal</h4>



<ul class="wp-block-list"><li>@click.self=”closeModal”</li></ul>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;div v-if="showModal"&gt;
    &lt;Modal :header="header" :text="text" theme="sale" @close="toggleModal" /&gt;
  &lt;/div&gt;
  &lt;button @click.alt="toggleModal"&gt;open modal (alt)&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!',
      showModal: false
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// Modal.vue

&lt;template&gt;
  &lt;div class="backdrop" @click.self="closeModal"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;h1&gt;{{ header }}&lt;/h1&gt;
      &lt;p&gt;{{ text }}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'header', 'text', 'theme'],
  methods: {
    closeModal() {
      this.$emit('close')
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Slots(插槽)</h3>



<pre class="wp-block-code"><code>// App.vue - 1

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;div v-if="showModal"&gt;
    &lt;Modal  theme="sale" @close="toggleModal"&gt;
      &lt;template v-slot:links&gt;
        &lt;a href="#"&gt;sign up now&lt;/a&gt;
        &lt;a href="#"&gt;more info&lt;/a&gt;
      &lt;/template&gt;
      &lt;h1&gt;Ninja Givaway&lt;/h1&gt;
      &lt;p&gt;Grab your ninja swag for half price!&lt;/p&gt;
    &lt;/Modal&gt;
  &lt;/div&gt;
  &lt;button @click.alt="toggleModal"&gt;open modal (alt)&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!',
      showModal: false
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 1

&lt;template&gt;
  &lt;div class="backdrop" @click.self="closeModal"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;slot&gt;&lt;/slot&gt;
      &lt;div class="actions"&gt;
        &lt;slot name="links"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'theme'],
  methods: {
    closeModal() {
      this.$emit('close')
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
  .modal .actions {
    text-align: center;
    margin: 30px 0 10px 0;
  }
  .modal .actions a {
    color: #333;
    padding: 8px;
    border: 1px solid #eee;
    border-radius: 4px;
    text-decoration: none;
    margin: 10px;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
  .modal.sale .actions {
    color: white;
  }
  .modal.sale .actions a {
    color: white;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue - 2

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;div v-if="showModal"&gt;
    &lt;Modal  theme="sale" @close="toggleModal"&gt;
      &lt;template v-slot:links&gt;
        &lt;a href="#"&gt;sign up now&lt;/a&gt;
        &lt;a href="#"&gt;more info&lt;/a&gt;
      &lt;/template&gt;

    &lt;/Modal&gt;
  &lt;/div&gt;
  &lt;button @click.alt="toggleModal"&gt;open modal (alt)&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!',
      showModal: false
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 2

&lt;template&gt;
  &lt;div class="backdrop" @click.self="closeModal"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;slot&gt;default content&lt;/slot&gt;
      &lt;div class="actions"&gt;
        &lt;slot name="links"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'theme'],
  methods: {
    closeModal() {
      this.$emit('close')
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
  .modal .actions {
    text-align: center;
    margin: 30px 0 10px 0;
  }
  .modal .actions a {
    color: #333;
    padding: 8px;
    border: 1px solid #eee;
    border-radius: 4px;
    text-decoration: none;
    margin: 10px;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
  .modal.sale .actions {
    color: white;
  }
  .modal.sale .actions a {
    color: white;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue - 3

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;div v-if="showModal"&gt;
    &lt;Modal  theme="" @close="toggleModal"&gt;
      &lt;template v-slot:links&gt;
        &lt;a href="#"&gt;sign up now&lt;/a&gt;
        &lt;a href="#"&gt;more info&lt;/a&gt;
      &lt;/template&gt;
      &lt;h1&gt;Ninja Givaway&lt;/h1&gt;
      &lt;p&gt;Grab your ninja swag for half price!&lt;/p&gt;
    &lt;/Modal&gt;
  &lt;/div&gt;
  &lt;button @click.alt="toggleModal"&gt;open modal (alt)&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      header: 'Sign up for the Giveway',
      text: 'Grab your ninja swag for half price!',
      showModal: false
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// Modal.vue - 3

&lt;template&gt;
  &lt;div class="backdrop" @click.self="closeModal"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;slot&gt;&lt;/slot&gt;
      &lt;div class="actions"&gt;
        &lt;slot name="links"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'theme'],
  methods: {
    closeModal() {
      this.$emit('close')
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
  .modal .actions {
    text-align: center;
    margin: 30px 0 10px 0;
  }
  .modal .actions a {
    color: #333;
    padding: 8px;
    border: 1px solid #eee;
    border-radius: 4px;
    text-decoration: none;
    margin: 10px;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
  .modal.sale .actions {
    color: white;
  }
  .modal.sale .actions a {
    color: white;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">CHALLENGE – Reusing Components</h3>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;div v-if="showModal"&gt;
    &lt;Modal  theme="" @close="toggleModal"&gt;
      &lt;template v-slot:links&gt;
        &lt;a href="#"&gt;sign up now&lt;/a&gt;
        &lt;a href="#"&gt;more info&lt;/a&gt;
      &lt;/template&gt;
      &lt;h1&gt;Ninja Givaway&lt;/h1&gt;
      &lt;p&gt;Grab your ninja swag for half price!&lt;/p&gt;
    &lt;/Modal&gt;
  &lt;/div&gt;

  &lt;div v-if="showModalTwo"&gt;
    &lt;Modal @close="toggleModalTwo"&gt;
      &lt;h1&gt;Sign up to the newsletter&lt;/h1&gt;
      &lt;p&gt;For updates and promo codes!&lt;/p&gt;
    &lt;/Modal&gt;
  &lt;/div&gt;
  &lt;button @click.alt="toggleModal"&gt;open modal (alt)&lt;/button&gt;
  &lt;button @click="toggleModalTwo"&gt;open modal&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
// challenge
// - create an extra button to poen a different modal
// - use the same modal component but pass in a different template (slot)
// - use a different method (e.g. toggleModalTwo) and data (e.g. showModalTwo)

import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      showModal: false,
      showModalTwo: false,
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    },
    toggleModalTwo() {
      this.showModalTwo = !this.showModalTwo
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// Modal.vue

&lt;template&gt;
  &lt;div class="backdrop" @click.self="closeModal"&gt;
    &lt;div class="modal" :class="{ sale: theme === 'sale'}"&gt;
      &lt;slot&gt;&lt;/slot&gt;
      &lt;div class="actions"&gt;
        &lt;slot name="links"&gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'theme'],
  methods: {
    closeModal() {
      this.$emit('close')
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .modal {
    width: 400px;
    padding: 20px;
    margin: 100px auto;
    background: white;
    border-radius: 10px;
  }
  .backdrop {
    top: 0;
    position: fixed;
    background: rgba(0,0,0,0.5);
    width: 100%;
    height: 100%;
  }
  .modal h1 {
    color: #03cfb4;
    border: none;
    padding: 0;
  }
  .modal p {
    font-style: normal;
  }
  .modal .actions {
    text-align: center;
    margin: 30px 0 10px 0;
  }
  .modal .actions a {
    color: #333;
    padding: 8px;
    border: 1px solid #eee;
    border-radius: 4px;
    text-decoration: none;
    margin: 10px;
  }

  .modal.sale {
    background: crimson;
    color: white;
  }
  .modal.sale h1 {
    color: white;
  }
  .modal.sale .actions {
    color: white;
  }
  .modal.sale .actions a {
    color: white;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Using Teleport (new features 新功能)</h3>



<ul class="wp-block-list"><li><a href="https://v3.vuejs.org/guide/teleport.html" target="_blank" rel="noreferrer noopener">Vue.js 3 – Teleport</a></li></ul>



<h4 class="wp-block-heading">class, id 都可以使用 e.g. – .modals、#modals</h4>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang=""&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width,initial-scale=1.0"&gt;
    &lt;link rel="icon" href="&lt;%= BASE_URL %&gt;favicon.ico"&gt;
    &lt;title&gt;&lt;%= htmlWebpackPlugin.options.title %&gt;&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;noscript&gt;
      &lt;strong&gt;We're sorry but &lt;%= htmlWebpackPlugin.options.title %&gt; doesn't work properly without JavaScript enabled. Please enable it to continue.&lt;/strong&gt;
    &lt;/noscript&gt;
    &lt;div id="app"&gt;&lt;/div&gt;
    &lt;!-- built files will be auto injected --&gt;
    &lt;div class="modals"&gt;&lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;{{ title }}&lt;/h1&gt;
  &lt;p&gt;Welcome...&lt;/p&gt;
  &lt;teleport to=".modals" v-if="showModal"&gt;
    &lt;Modal  theme="" @close="toggleModal"&gt;
      &lt;template v-slot:links&gt;
        &lt;a href="#"&gt;sign up now&lt;/a&gt;
        &lt;a href="#"&gt;more info&lt;/a&gt;
      &lt;/template&gt;
      &lt;h1&gt;Ninja Givaway&lt;/h1&gt;
      &lt;p&gt;Grab your ninja swag for half price!&lt;/p&gt;
    &lt;/Modal&gt;
  &lt;/teleport&gt;

  &lt;teleport to=".modals" v-if="showModalTwo"&gt;
    &lt;Modal @close="toggleModalTwo"&gt;
      &lt;h1&gt;Sign up to the newsletter&lt;/h1&gt;
      &lt;p&gt;For updates and promo codes!&lt;/p&gt;
    &lt;/Modal&gt;
  &lt;/teleport&gt;

  &lt;button @click.alt="toggleModal"&gt;open modal (alt)&lt;/button&gt;
  &lt;button @click="toggleModalTwo"&gt;open modal&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
// challenge
// - create an extra button to poen a different modal
// - use the same modal component but pass in a different template (slot)
// - use a different method (e.g. toggleModalTwo) and data (e.g. showModalTwo)

import Modal from './components/Modal.vue'

export default {
  name: 'App',
  components: { Modal },
  data() {
    return {
      title: 'My First Vue App :)',
      showModal: false,
      showModalTwo: false,
    }
  },
  methods: {
    toggleModal() {
      this.showModal = !this.showModal
    },
    toggleModalTwo() {
      this.showModalTwo = !this.showModalTwo
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app, .modals {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1 {
  border-bottom: 1px solid #ddd;
  display: inline-block;
  padding-bottom: 10px;
}
&lt;/style&gt;</code></pre>



<h2 class="wp-block-heading">第4節：PROJECT BUILD – Reaction Timer</h2>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Project 1 Preview &amp; Setup</h3>



<h4 class="wp-block-heading">Reaction Timer Project</h4>



<ul class="wp-block-list"><li>App.vue<ul><li>Block.vue</li><li>Results.vue</li></ul></li></ul>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>打開終端機</li><li>移動到要安裝位置</li><li>輸入指令：vue create reaction-timer</li><li>Please pick a preset: Manually select features</li><li>Check the features needed for your project: Choose Vue version, Babel</li><li>Choose a version of Vue.js that you want to start the project with 3.x</li><li>Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config file</li><li>Save this as a preset for future projects? N</li><li>移動到檔案位置：cd reaction-timer</li><li>打開 VSCode：code .</li><li>移除 HelloWorld.vue 檔案</li><li>修改 App.vue 檔案、移除不用的內容</li><li>在 components 資料夾裡新增 Block.vue、Results.vue 檔案</li><li>打開終端機、執行指令：npm run serve</li></ul>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;Ninja Reaction Timer&lt;/h1&gt;
&lt;/template&gt;

&lt;script&gt;


export default {
  name: 'App',
  components: { }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #444;
  margin-top: 60px;
}
&lt;/style&gt;
</code></pre>



<h4 class="wp-block-heading">參考文件</h4>



<ul class="wp-block-list"><li><a href="https://router.vuejs.org/guide/essentials/history-mode.html" target="_blank" rel="noreferrer noopener">Vue Router Guide – Different History modes</a></li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Starting a New Game</h3>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;Ninja Reaction Timer&lt;/h1&gt;
  &lt;button @click="start" :disabled="isPlaying"&gt;play&lt;/button&gt;
  &lt;Block v-if="isPlaying" :delay="delay" /&gt;
&lt;/template&gt;

&lt;script&gt;
import Block from './components/Block.vue'

export default {
  name: 'App',
  components: { Block },
  data() {
    return {
      isPlaying: false,
      delay: null
    }
  },
  methods: {
    start() {
      this.delay = 2000 + Math.random() * 5000
      this.isPlaying = true
      // console.log(this.delay)
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #444;
  margin-top: 60px;
}
&lt;/style&gt;
</code></pre>



<ul class="wp-block-list"><li>快速建立 &lt;template&gt;、&lt;script&gt;、&lt;style&gt;，快捷鍵 &lt;vue&gt; with default.vue</li></ul>



<pre class="wp-block-code"><code>// Block.vue

&lt;template&gt;
  &lt;div class="block"&gt;
    click me
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'delay']
}
&lt;/script&gt;

&lt;style&gt;
  .block {
    width: 400px;
    border-radius: 20px;
    background: #0faf87;
    color: white;
    text-align: center;
    padding: 100px 0;
    margin: 40px auto;
  }
&lt;/style&gt;</code></pre>



<h4 class="wp-block-heading">Vetur can’t find&nbsp;<code>tsconfig.json</code>,&nbsp;<code>jsconfig.json</code>&nbsp;in /xxxx/xxxxxx.</h4>



<ul class="wp-block-list"><li><a href="https://vuejs.github.io/vetur/guide/FAQ.html#vetur-can-t-find-tsconfig-json-jsconfig-json-in-xxxx-xxxxxx" target="_blank" rel="noreferrer noopener">Vetur 文件</a></li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Component Lifecycle Hooks (組件生命週期鉤子) 重要!</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://v3.vuejs.org/guide/instance.html#lifecycle-hooks" target="_blank">Lifecycle Diagram</a></li></ul>



<pre class="wp-block-code"><code>// Block.vue

&lt;template&gt;
  &lt;div class="block" v-if="showBlock"&gt;
    click me
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'delay'],
  data() {
    return {
      showBlock: false
    }
  },
  mounted() {
    // console.log('component mounted')
    setTimeout(() =&gt; {
      this.showBlock = true
      // console.log(this.delay)
    }, this.delay)
  },
  // updated() {
  //   console.log('component updated')
  // },
  // unmounted() {
  //   console.log('unmounted')
  // }
}
&lt;/script&gt;

&lt;style&gt;
  .block {
    width: 400px;
    border-radius: 20px;
    background: #0faf87;
    color: white;
    text-align: center;
    padding: 100px 0;
    margin: 40px auto;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;Ninja Reaction Timer&lt;/h1&gt;
  &lt;button @click="start" :disabled="isPlaying"&gt;play&lt;/button&gt;
  &lt;Block v-if="isPlaying" :delay="delay" /&gt;
&lt;/template&gt;

&lt;script&gt;
import Block from './components/Block.vue'

export default {
  name: 'App',
  components: { Block },
  data() {
    return {
      isPlaying: false,
      delay: null
    }
  },
  methods: {
    start() {
      this.delay = 2000 + Math.random() * 5000
      this.isPlaying = true
      // console.log(this.delay)
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #444;
  margin-top: 60px;
}
&lt;/style&gt;
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Creating the Reaction Timer</h3>



<pre class="wp-block-code"><code>// Block.vue

&lt;template&gt;
  &lt;div class="block" v-if="showBlock" @click="stopTimer"&gt;
    click me
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'delay'],
  data() {
    return {
      showBlock: false,
      timer: null,
      reactionTime: 0
    }
  },
  mounted() {
    setTimeout(() =&gt; {
      this.showBlock = true
      this.startTimer()
    }, this.delay)
  },
  methods: {
    startTimer() {
      this.timer = setInterval(() =&gt; {
        this.reactionTime += 10
      }, 10)
    },
    stopTimer() {
      clearInterval(this.timer)
      console.log(this.reactionTime)
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .block {
    width: 400px;
    border-radius: 20px;
    background: #0faf87;
    color: white;
    text-align: center;
    padding: 100px 0;
    margin: 40px auto;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Emitting Custom Events with Data</h3>



<pre class="wp-block-code"><code>// Block.vue

&lt;template&gt;
  &lt;div class="block" v-if="showBlock" @click="stopTimer"&gt;
    click me
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'delay'],
  data() {
    return {
      showBlock: false,
      timer: null,
      reactionTime: 0
    }
  },
  mounted() {
    setTimeout(() =&gt; {
      this.showBlock = true
      this.startTimer()
    }, this.delay)
  },
  methods: {
    startTimer() {
      this.timer = setInterval(() =&gt; {
        this.reactionTime += 10
      }, 10)
    },
    stopTimer() {
      clearInterval(this.timer)
      this.$emit('end', this.reactionTime)
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .block {
    width: 400px;
    border-radius: 20px;
    background: #0faf87;
    color: white;
    text-align: center;
    padding: 100px 0;
    margin: 40px auto;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;Ninja Reaction Timer&lt;/h1&gt;
  &lt;button @click="start" :disabled="isPlaying"&gt;play&lt;/button&gt;
  &lt;Block v-if="isPlaying" :delay="delay" @end="endGame" /&gt;
  &lt;p v-if="showResults"&gt;Reaction time: {{ score }} ms&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
import Block from './components/Block.vue'

export default {
  name: 'App',
  components: { Block },
  data() {
    return {
      isPlaying: false,
      delay: null,
      score: null,
      showResults: false
    }
  },
  methods: {
    start() {
      this.delay = 2000 + Math.random() * 5000
      this.isPlaying = true
      this.showResults = false
    },
    endGame(reactionTime) {
      this.score = reactionTime
      this.isPlaying = false
      this.showResults = true
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #444;
  margin-top: 60px;
}
&lt;/style&gt;
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">CHALLENGE – Showing a Results Component</h3>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;Ninja Reaction Timer&lt;/h1&gt;
  &lt;button @click="start" :disabled="isPlaying"&gt;play&lt;/button&gt;
  &lt;Block v-if="isPlaying" :delay="delay" @end="endGame" /&gt;
  &lt;Results v-if="showResults" :score="score" /&gt;
&lt;/template&gt;

&lt;script&gt;
// Challenge
// - when the game ends, show the results component
// - output the score inside the results component

import Block from './components/Block.vue'
import Results from './components/Results.vue'

export default {
  name: 'App',
  components: { Block, Results },
  data() {
    return {
      isPlaying: false,
      delay: null,
      score: null,
      showResults: false
    }
  },
  methods: {
    start() {
      this.delay = 2000 + Math.random() * 5000
      this.isPlaying = true
      this.showResults = false
    },
    endGame(reactionTime) {
      this.score = reactionTime
      this.isPlaying = false
      this.showResults = true
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #444;
  margin-top: 60px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Results.vue

&lt;template&gt;
  &lt;p&gt;Reaction time - {{ score }} ms&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'score']
}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<h3 class="wp-block-heading">Finishing Touches</h3>



<pre class="wp-block-code"><code>// Results.vue

&lt;template&gt;
  &lt;p&gt;Reaction time - {{ score }} ms&lt;/p&gt;
  &lt;p class="rank"&gt;{{ rank }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'score'],
  data() {
    return {
      rank: null
    }
  },
  mounted() {
    if (this.score &lt; 250) {
      this.rank = 'Ninja Fingers'
    }
    else if (this.score &lt; 400) {
      this.rank = 'Rapid Reflexes'
    }
    else {
      this.rank = 'Snail pace...'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .rank {
    font-size: 1.4em;
    color: #0faf87;
    font-weight: bold;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue

&lt;template&gt;
  &lt;h1&gt;Ninja Reaction Timer&lt;/h1&gt;
  &lt;button @click="start" :disabled="isPlaying"&gt;play&lt;/button&gt;
  &lt;Block v-if="isPlaying" :delay="delay" @end="endGame" /&gt;
  &lt;Results v-if="showResults" :score="score" /&gt;
&lt;/template&gt;

&lt;script&gt;
// Challenge
// - when the game ends, show the results component
// - output the score inside the results component

import Block from './components/Block.vue'
import Results from './components/Results.vue'

export default {
  name: 'App',
  components: { Block, Results },
  data() {
    return {
      isPlaying: false,
      delay: null,
      score: null,
      showResults: false
    }
  },
  methods: {
    start() {
      this.delay = 2000 + Math.random() * 5000
      this.isPlaying = true
      this.showResults = false
    },
    endGame(reactionTime) {
      this.score = reactionTime
      this.isPlaying = false
      this.showResults = true
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #444;
  margin-top: 60px;
}
button {
  background: #0faf87;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;
  letter-spacing: 1px;
  cursor: pointer;
  margin: 10px;
}
button&#91;disabled] {
  opacity: 0.2;
  cursor: not-allowed;
}
&lt;/style&gt;
</code></pre>



<h2 class="wp-block-heading">第5節：Forms &amp; Data Binding</h2>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Forms Intro &amp; Setup (表單介紹 &amp; 安裝)</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>打開終端機(Terminal)</li><li>移動到要安裝專案的位置</li><li>建立專案、輸入指令 vue create web-form</li><li>Please pick a preset: Manually select features</li><li>Check the features needed for your project: Choose Vue version, Babel</li><li>Choose a version of Vue.js that you want to start the project with 3.x (Preview)</li><li>Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files</li><li>Save this as a preset for the future projects? N</li><li>建立完成後、移動到專案位置 cd web-form</li><li>輸入指令、打開網頁編輯器：code .</li><li>移除 components/HelloWorld.vue 檔案</li><li>修改 App.vue 檔案內容</li><li>新增檔案 SignupForm.vue 在 components 資料夾裡面、新增內容</li><li>執行指令：npm run serve</li><li>在這之前要記得在 App.vue 檔案 import SignupForm from ‘./components/SignupForm.vue’</li><li>在 App.vue 做些內容調整</li></ul>



<pre class="wp-block-code"><code>// App.vue - 1

&lt;template&gt;

&lt;/template&gt;

&lt;script&gt;
export default {
  name: 'App',
  components: {}
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
body {
  margin: 0;
  background: #eee;
}
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required&gt;
  &lt;/form&gt;
&lt;/template&gt;

&lt;script&gt;
export default {

}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// App.vue - 2

&lt;template&gt;
  &lt;SignupForm /&gt;
&lt;/template&gt;

&lt;script&gt;
import SignupForm from './components/SignupForm.vue'

export default {
  name: 'App',
  components: { SignupForm }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
body {
  margin: 0;
  background: #eee;
}
&lt;/style&gt;</code></pre>



<h3 class="wp-block-heading">Two-way Data Binding (雙向綁定)</h3>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required v-model="email"&gt;

    &lt;label&gt;Password:&lt;/label&gt;
    &lt;input type="password" required v-model="password"&gt;
    
  &lt;/form&gt;
  &lt;p&gt;Email: {{ email }}&lt;/p&gt;
  &lt;p&gt;Password: {{ password }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      email: 'mario',
      password: ''
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
&lt;/style&gt;</code></pre>



<h4 class="wp-block-heading">Vue.js 文件</h4>



<ul class="wp-block-list"><li><a href="https://v3.vuejs.org/api/directives.html#v-model" target="_blank" rel="noreferrer noopener">v-model</a></li></ul>



<h3 class="wp-block-heading">Select Fields</h3>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required v-model="email"&gt;

    &lt;label&gt;Password:&lt;/label&gt;
    &lt;input type="password" required v-model="password"&gt;
    
    &lt;label&gt;Role:&lt;/label&gt;
    &lt;select v-model="role"&gt;
      &lt;option value="developer"&gt;Web Developer&lt;/option&gt;
      &lt;option value="designer"&gt;Web Designer&lt;/option&gt;
    &lt;/select&gt;

  &lt;/form&gt;
  &lt;p&gt;Email: {{ email }}&lt;/p&gt;
  &lt;p&gt;Password: {{ password }}&lt;/p&gt;
  &lt;p&gt;Role: {{ role }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      email: 'mario',
      password: '',
      role: 'designer'
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input, select {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
&lt;/style&gt;</code></pre>



<h3 class="wp-block-heading">Checkboxes</h3>



<h4 class="wp-block-heading">使用方式</h4>



<ul class="wp-block-list"><li>Boolean (布林值)</li><li>Array (陣列)</li></ul>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required v-model="email"&gt;

    &lt;label&gt;Password:&lt;/label&gt;
    &lt;input type="password" required v-model="password"&gt;
    
    &lt;label&gt;Role:&lt;/label&gt;
    &lt;select v-model="role"&gt;
      &lt;option value="developer"&gt;Web Developer&lt;/option&gt;
      &lt;option value="designer"&gt;Web Designer&lt;/option&gt;
    &lt;/select&gt;

    &lt;div class="terms"&gt;
      &lt;input type="checkbox" v-model="terms" required&gt;
      &lt;label&gt;Accept terms and conditions&lt;/label&gt;
    &lt;/div&gt;

    &lt;!-- &lt;div&gt;
      &lt;input type="checkbox" value="shaun" v-model="names"&gt;
      &lt;label&gt;Shaun&lt;/label&gt;
    &lt;/div&gt;
    &lt;div&gt;
      &lt;input type="checkbox" value="yoshi" v-model="names"&gt;
      &lt;label&gt;Yoshi&lt;/label&gt;
    &lt;/div&gt;
    &lt;div&gt;
      &lt;input type="checkbox" value="mario" v-model="names"&gt;
      &lt;label&gt;Mario&lt;/label&gt;
    &lt;/div&gt; --&gt;

  &lt;/form&gt;
  &lt;p&gt;Email: {{ email }}&lt;/p&gt;
  &lt;p&gt;Password: {{ password }}&lt;/p&gt;
  &lt;p&gt;Role: {{ role }}&lt;/p&gt;
  &lt;p&gt;Terms accepted: {{ terms }}&lt;/p&gt;
  &lt;!-- &lt;p&gt;Names: {{ names }}&lt;/p&gt; --&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      email: 'mario',
      password: '',
      role: 'designer',
      terms: false,
      // names: &#91;]
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input, select {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
  input&#91;type="checkbox"] {
    display: inline-block;
    width: 16px;
    margin: 0 10px 0 0;
    position: relative;
    top: 2px;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Keyboard Events &amp; Modifiers</h3>



<ul class="wp-block-list"><li>@keypress</li><li>@keydown</li><li>@keyup</li></ul>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required v-model="email"&gt;

    &lt;label&gt;Password:&lt;/label&gt;
    &lt;input type="password" required v-model="password"&gt;
    
    &lt;label&gt;Role:&lt;/label&gt;
    &lt;select v-model="role"&gt;
      &lt;option value="developer"&gt;Web Developer&lt;/option&gt;
      &lt;option value="designer"&gt;Web Designer&lt;/option&gt;
    &lt;/select&gt;

    &lt;label&gt;Skills:&lt;/label&gt;
    &lt;input type="text" v-model="tempSkill" @keyup.alt="addSkill"&gt;
    &lt;div v-for="skill in skills" :key="skill" class="pill"&gt;
      {{ skill }}
    &lt;/div&gt;

    &lt;div class="terms"&gt;
      &lt;input type="checkbox" v-model="terms" required&gt;
      &lt;label&gt;Accept terms and conditions&lt;/label&gt;
    &lt;/div&gt;

  &lt;/form&gt;
  &lt;p&gt;Email: {{ email }}&lt;/p&gt;
  &lt;p&gt;Password: {{ password }}&lt;/p&gt;
  &lt;p&gt;Role: {{ role }}&lt;/p&gt;
  &lt;p&gt;Terms accepted: {{ terms }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      email: 'mario',
      password: '',
      role: 'designer',
      terms: false,
      tempSkill: '',
      skills: &#91;]
    }
  },
  methods: {
    addSkill(e) {
      // console.log(e)
      if (e.key === ',' &amp;&amp; this.tempSkill) {
        if (!this.skills.includes(this.tempSkill)) {
          this.skills.push(this.tempSkill)
        }
        this.tempSkill = ''
      }
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input, select {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
  input&#91;type="checkbox"] {
    display: inline-block;
    width: 16px;
    margin: 0 10px 0 0;
    position: relative;
    top: 2px;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">CHALLENGE – Deleting Skills</h3>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required v-model="email"&gt;

    &lt;label&gt;Password:&lt;/label&gt;
    &lt;input type="password" required v-model="password"&gt;
    
    &lt;label&gt;Role:&lt;/label&gt;
    &lt;select v-model="role"&gt;
      &lt;option value="developer"&gt;Web Developer&lt;/option&gt;
      &lt;option value="designer"&gt;Web Designer&lt;/option&gt;
    &lt;/select&gt;

    &lt;label&gt;Skills:&lt;/label&gt;
    &lt;input type="text" v-model="tempSkill" @keyup.alt="addSkill"&gt;
    &lt;div v-for="skill in skills" :key="skill" class="pill"&gt;
      &lt;span @click="deleteSkill(skill)"&gt;{{ skill }}&lt;/span&gt;
    &lt;/div&gt;

    &lt;div class="terms"&gt;
      &lt;input type="checkbox" v-model="terms" required&gt;
      &lt;label&gt;Accept terms and conditions&lt;/label&gt;
    &lt;/div&gt;

  &lt;/form&gt;
  &lt;p&gt;Email: {{ email }}&lt;/p&gt;
  &lt;p&gt;Password: {{ password }}&lt;/p&gt;
  &lt;p&gt;Role: {{ role }}&lt;/p&gt;
  &lt;p&gt;Terms accepted: {{ terms }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
// challenge
// - when a user clicks on a skill, delete that skill

export default {
  data() {
    return {
      email: 'mario',
      password: '',
      role: 'designer',
      terms: false,
      tempSkill: '',
      skills: &#91;]
    }
  },
  methods: {
    addSkill(e) {
      if (e.key === ',' &amp;&amp; this.tempSkill) {
        if (!this.skills.includes(this.tempSkill)) {
          this.skills.push(this.tempSkill)
        }
        this.tempSkill = ''
      }
    },
    deleteSkill(skill) {
      this.skills = this.skills.filter((item) =&gt;{
        return skill !== item
      })
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input, select {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
  input&#91;type="checkbox"] {
    display: inline-block;
    width: 16px;
    margin: 0 10px 0 0;
    position: relative;
    top: 2px;
  }
  .pill {
    display: inline-block;
    margin: 20px 10px 0 0;
    background: #eee;
    border-radius: 20px;
    font-size: 12px;
    letter-spacing: 1px;
    font-weight: bold;
    color: #777;
    cursor: pointer;
  }
&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Submitting the Form</h3>



<pre class="wp-block-code"><code>// SignupForm.vue

&lt;template&gt;
  &lt;form @submit.prevent="handleSubmit"&gt;
    &lt;label&gt;Email:&lt;/label&gt;
    &lt;input type="email" required v-model="email"&gt;

    &lt;label&gt;Password:&lt;/label&gt;
    &lt;input type="password" required v-model="password"&gt;
    &lt;div v-if="passwordError" class="error"&gt;{{ passwordError }}&lt;/div&gt;
    
    &lt;label&gt;Role:&lt;/label&gt;
    &lt;select v-model="role"&gt;
      &lt;option value="developer"&gt;Web Developer&lt;/option&gt;
      &lt;option value="designer"&gt;Web Designer&lt;/option&gt;
    &lt;/select&gt;

    &lt;label&gt;Skills:&lt;/label&gt;
    &lt;input type="text" v-model="tempSkill" @keyup.alt="addSkill"&gt;
    &lt;div v-for="skill in skills" :key="skill" class="pill"&gt;
      &lt;span @click="deleteSkill(skill)"&gt;{{ skill }}&lt;/span&gt;
    &lt;/div&gt;

    &lt;div class="terms"&gt;
      &lt;input type="checkbox" v-model="terms" required&gt;
      &lt;label&gt;Accept terms and conditions&lt;/label&gt;
    &lt;/div&gt;

    &lt;div class="submit"&gt;
      &lt;button&gt;Create an Account&lt;/button&gt;
    &lt;/div&gt;

  &lt;/form&gt;
  &lt;p&gt;Email: {{ email }}&lt;/p&gt;
  &lt;p&gt;Password: {{ password }}&lt;/p&gt;
  &lt;p&gt;Role: {{ role }}&lt;/p&gt;
  &lt;p&gt;Terms accepted: {{ terms }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
// challenge
// - when a user clicks on a skill, delete that skill

export default {
  data() {
    return {
      email: '',
      password: '',
      role: 'designer',
      terms: false,
      tempSkill: '',
      skills: &#91;],
      passwordError: ''
    }
  },
  methods: {
    addSkill(e) {
      if (e.key === ',' &amp;&amp; this.tempSkill) {
        if (!this.skills.includes(this.tempSkill)) {
          this.skills.push(this.tempSkill)
        }
        this.tempSkill = ''
      }
    },
    deleteSkill(skill) {
      this.skills = this.skills.filter((item) =&gt;{
        return skill !== item
      })
    },
    handleSubmit() {
      // console.log('form submitted')
      // validate password
      this.passwordError = this.password.length &gt; 5 ?
        '' : 'Password must be at least 6 chars long'

      if(!this.passwordError) {
        console.log('email: ', this.email)
        console.log('password: ', this.password)
        console.log('role: ', this.role)
        console.log('skills: ', this.skills)
        console.log('terms accepted: ', this.terms)
      }
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  form {
    max-width: 420px;
    margin: 30px auto;
    background: white;
    text-align: left;
    padding: 40px;
    border-radius: 10px;
  }
  label {
    color: #aaa;
    display: inline-block;
    margin: 25px 0 15px;
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
  }
  input, select {
    display: block;
    padding: 10px 6px;
    width: 100%;
    box-sizing: border-box;
    border: none;
    border-bottom: 1px solid #ddd;
    color: #555;
  }
  input&#91;type="checkbox"] {
    display: inline-block;
    width: 16px;
    margin: 0 10px 0 0;
    position: relative;
    top: 2px;
  }
  .pill {
    display: inline-block;
    margin: 20px 10px 0 0;
    background: #eee;
    border-radius: 20px;
    font-size: 12px;
    letter-spacing: 1px;
    font-weight: bold;
    color: #777;
    cursor: pointer;
  }
  button {
    background: #0b6dff;
    border: 0;
    padding: 10px 20px;
    margin-top: 20px;
    color: white;
    border-radius: 20px;
  }
  .submit {
    text-align: center;
  }
  .error {
    color: #ff0062;
    margin-top: 10px;
    font-size: 0.8em;
    font-weight: bold;
  }
&lt;/style&gt;</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">第6節：Vue Router Basics</h2>



<p>Vue 路由可重複觀看、練習。</p>



<h3 class="wp-block-heading">Why Use the Vue Router?</h3>



<p>可重複觀看了解觀念。</p>



<h4 class="wp-block-heading">The Vue Router (圖片講解)</h4>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-3 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="753" height="611" data-id="558" src="/wordpress_blog/wp-content/uploads/2022/04/The-Vue-Router-01.png" alt="" class="wp-image-558"/><figcaption>The Vue Router 01</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1791" height="563" data-id="557" src="/wordpress_blog/wp-content/uploads/2022/04/The-Vue-Router-02.png" alt="" class="wp-image-557"/><figcaption>The Vue Router 02</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1785" height="581" data-id="560" src="/wordpress_blog/wp-content/uploads/2022/04/The-Vue-Router-03.png" alt="" class="wp-image-560"/><figcaption>The Vue Router 03</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1737" height="563" data-id="562" src="/wordpress_blog/wp-content/uploads/2022/04/The-Vue-Router-04.png" alt="" class="wp-image-562"/><figcaption>The Vue Router 04</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1771" height="569" data-id="559" src="/wordpress_blog/wp-content/uploads/2022/04/The-Vue-Router-05.png" alt="" class="wp-image-559"/><figcaption>The Vue Router 05</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1171" height="611" data-id="561" src="/wordpress_blog/wp-content/uploads/2022/04/The-Vue-Router-06.png" alt="" class="wp-image-561"/><figcaption>The Vue Router 06</figcaption></figure>
</figure>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Router Setup for New Projects</h3>



<p>很重要，可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>開啟終端機、移動到要建立專案資料夾的位置、輸入終端機指令 vue create ninja-jobs</li><li>Vue CLI v4.5.8 (我的版本是 Vue CLI v4.5.15)</li><li>Please pick a preset: Manually select feature</li><li>Check the features needed for your project: Choose Vue version, Babel, Router</li><li>Choose a version of Vue.js that you want to start the project with 3.x (Preview)</li><li>Use history mode for router? (Requires proper server setup for index fallback in production) Yes</li><li>Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files</li><li>Save this as a preset for future projects? n</li><li>移動到專案資料夾位置 cd ninja-jobs</li><li>打開 VSCode 輸入指令 code .</li><li>首先看新的樣板專案在 package.json 檔案，擁有新的 dependency 是 “vue-router”</li><li>第二個不同的地方在 src 資料夾，有 router 資料夾、裡面有 index.js 檔案，可以看到有 routes 陣列、裡面的物件有 path, name, component 三個屬性</li><li>在 src/router/index.js 修改 about</li><li>介紹 src/main.js</li><li>介紹 App.vue &lt;router-view/&gt;</li><li>打開終端機，執行指令 npm run serve</li><li>使用 Ctrl + 滑鼠左鍵打開本地端網頁</li></ol>



<pre class="wp-block-code"><code>// src/router/index.js - 13 修改 about

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Router Links (路由連結)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>講解 router link 和 a 連結的差別，用 Google Network 說明 router link 並不是向伺服器請求</li><li>App.vue 把 a 連結移除、然後儲存，查看 Google Elements 的 a 連結可以看到類別在變動，把 App.vue 的 #nav a.router-link-exact-active 修改、#nav a 修改</li><li>App.vue router link to 也可以改成 :to 使用，同樣都能運作，並說明使用上的差異，在路徑需要更新上使用 :to 會比較好，因為 name 不用更動</li><li>移除專案用不到的內容，到 Home.vue 檔案、移除 import HelloWorld、移除 components/HelloWorld.vue 檔案</li><li>在 About.vue 修改內容</li></ol>



<pre class="wp-block-code"><code>// App.vue - 1

&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt; |
    &lt;a href="/about"&gt;about&lt;/a&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue - 2 修改


&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link to="/about"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 4px;
}

#nav a.router-link-exact-active {
  color: #fff;
  background: crimson;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue - 3 講解 to 的使用方法

&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'About' }"&gt;About&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 4px;
}

#nav a.router-link-exact-active {
  color: #fff;
  background: crimson;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// Home.vue - 4 移除 import HelloWorld、修改 &lt;template&gt; 內容、移除 components 裡面的 HelloWorld

&lt;template&gt;
  &lt;div class="home"&gt;
    &lt;h1&gt;Homepage&lt;/h1&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia incidunt accusantium deserunt praesentium debitis voluptate, ex numquam facere ut sed, odio quasi amet sint quae unde. Porro accusamus nobis eius? Perferendis magnam rem possimus natus ducimus pariatur expedita ad sequi minima. Eum ex vero impedit dolores corporis voluptate architecto, sit commodi, quaerat nihil laborum, repellendus accusamus. Minus quaerat labore soluta error consectetur voluptates placeat eum maiores. Ullam delectus omnis dolorum unde cupiditate officiis repudiandae nostrum? Qui est possimus maiores. Est, nihil deleniti, voluptate tenetur pariatur distinctio, et earum minima quo ullam libero quae atque delectus voluptatum esse ut. Ut, et?&lt;/p&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia incidunt accusantium deserunt praesentium debitis voluptate, ex numquam facere ut sed, odio quasi amet sint quae unde. Porro accusamus nobis eius? Perferendis magnam rem possimus natus ducimus pariatur expedita ad sequi minima. Eum ex vero impedit dolores corporis voluptate architecto, sit commodi, quaerat nihil laborum, repellendus accusamus. Minus quaerat labore soluta error consectetur voluptates placeat eum maiores. Ullam delectus omnis dolorum unde cupiditate officiis repudiandae nostrum? Qui est possimus maiores. Est, nihil deleniti, voluptate tenetur pariatur distinctio, et earum minima quo ullam libero quae atque delectus voluptatum esse ut. Ut, et?&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: 'Home',
  components: { }
}
&lt;/script&gt;
</code></pre>



<pre class="wp-block-code"><code>// About.vue - 5 修改內容

&lt;template&gt;
  &lt;div class="about"&gt;
    &lt;h1&gt;This is an about page&lt;/h1&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellat placeat debitis impedit repudiandae adipisci incidunt veritatis sit praesentium nulla. Laudantium numquam eos error. Laborum laudantium necessitatibus fugiat et obcaecati fuga repellendus? Consequatur mollitia eos expedita itaque suscipit corrupti nesciunt ducimus sint odio possimus architecto ipsa debitis, explicabo distinctio amet optio, iure tenetur quas ipsum, cum nam dicta quasi dolore at? Eligendi placeat facere iusto dignissimos delectus fugiat incidunt temporibus, veniam cum. Unde quaerat iste vero deserunt dicta facilis consequatur fugit omnis? Dicta reiciendis a rem culpa iste aliquam porro dolores, laudantium obcaecati veniam tenetur fugiat facilis voluptates eos excepturi quo?&lt;/p&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Repellat placeat debitis impedit repudiandae adipisci incidunt veritatis sit praesentium nulla. Laudantium numquam eos error. Laborum laudantium necessitatibus fugiat et obcaecati fuga repellendus? Consequatur mollitia eos expedita itaque suscipit corrupti nesciunt ducimus sint odio possimus architecto ipsa debitis, explicabo distinctio amet optio, iure tenetur quas ipsum, cum nam dicta quasi dolore at? Eligendi placeat facere iusto dignissimos delectus fugiat incidunt temporibus, veniam cum. Unde quaerat iste vero deserunt dicta facilis consequatur fugit omnis? Dicta reiciendis a rem culpa iste aliquam porro dolores, laudantium obcaecati veniam tenetur fugiat facilis voluptates eos excepturi quo?&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Folder Structure (資料夾結構)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>在 views 資料夾建立 Jobs.vue 檔案、內容，輸入 &lt;vue&gt; 出現下拉提示快捷建立</li><li>在 src/router/index.js 新增 routes 物件、import Jobs</li><li>在 App.vue 新增 Jobs 的 &lt;router-link&gt;</li><li>在 views 資料夾建立 JobDetails.vue 檔案，因為 Jobs.vue 與 JobDetails.vue 同類型的區域，會在 views 資料夾建立 jobs 資料夾、並把 Jobs.vue、JobDetails.vue 移動到 jobs 資料夾裡面，在移動後會產生錯誤，因此在 index.js 檔案要修改 Jobs.vue 位置</li></ol>



<pre class="wp-block-code"><code>// Jobs.vue - 1 新增內容

&lt;template&gt;
  &lt;h1&gt;Jobs&lt;/h1&gt;
  &lt;div v-for="job in jobs" :key="job.id"&gt;
    &lt;h2&gt;{{ job.title }}&lt;/h2&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      jobs: &#91;
        { title: 'Ninja UX Designer', id: 1, details: 'lorem'},
        { title: 'Ninja Web Developer', id: 2, details: 'lorem'},
        { title: 'Ninja Vue Developer', id: 3, details: 'lorem'}
      ]
    }
  }
}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// src/router/Jobs.vue - 2 新增 route 物件、import Jobs

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Jobs from '../views/Jobs.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<pre class="wp-block-code"><code>// App.vue - 3 新增 Jobs 的 &lt;router-link&gt;

&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'About' }"&gt;About&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'Jobs' }"&gt;Jobs&lt;/router-link&gt;
  &lt;/div&gt;
  &lt;router-view/&gt;
&lt;/template&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 4px;
}

#nav a.router-link-exact-active {
  color: #fff;
  background: crimson;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// index.js - 修改 import Jobs 的位置

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Jobs from '../views/jobs/Jobs.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Route Parameters (路由參數)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>講解 Route Parameters 觀念</li><li>在 src/router/index.js 新增 routes 物件、新增 import JobsDetails，然後儲存</li><li>在 JobDetails.vue 新增內容，然後到網頁連結加上 /1，就會顯示 Job Details Page 頁面，不論 / 後面加上什麼都會顯示 Job Details Page 頁面</li><li>接著在 JobDetails.vue 新增 p 段落，(:id 的命名可以客製化)，然後儲存、到網頁畫面可以看到 p 段落內容會與 /jobs/:id 一起更動</li><li>在 JobDetails.vue 我們也可以使用 export default 物件，也可以做相同的事情</li></ol>



<h4 class="wp-block-heading">Route Parameters</h4>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1757" height="707" src="/wordpress_blog/wp-content/uploads/2022/04/Route-Parameters.png" alt="" class="wp-image-564"/><figcaption>Route Parameters</figcaption></figure>



<pre class="wp-block-code"><code>// src/router/index.js - 2 新增 routes 物件、新增 import JobDetails

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Jobs from '../views/jobs/Jobs.vue'
import JobDetails from '../views/jobs/JobDetails.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  },
  {
    path: '/jobs/:id',
    name: 'JobDetails',
    component: JobDetails
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<pre class="wp-block-code"><code>// JobDetails.vue - 3 新增內容

&lt;template&gt;
  &lt;h1&gt;Job Details Page&lt;/h1&gt;
&lt;/template&gt;

&lt;script&gt;
export default {

}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// JobDetails.vue - 4 新增 p 段落

&lt;template&gt;
  &lt;h1&gt;Job Details Page&lt;/h1&gt;
  &lt;p&gt;The job id is {{ $route.params.id }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {

}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// JobDetails.vue - 5 使用 export default 做相同的事情

&lt;template&gt;
  &lt;h1&gt;Job Details Page&lt;/h1&gt;
  &lt;p&gt;The job id is {{ id }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      id: this.$route.params.id
    }
  }
}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Dynamic Links (動態連結)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>在 Jobs.vue 把 &lt;h2&gt; 剪下，新增 &lt;router-link&gt;、把 &lt;h2&gt; 貼上，然後增加 :to 屬性</li><li>到網頁畫面 Google 檢視 Elements 可以看到 &lt;a&gt; href 屬性有 /jobs/1、/jobs/2、/jobs/3</li><li>在 JobDetails.vue export default 新增 props 屬性</li><li>在 index.js routes 的 JobDetails 那個物件新增 props 屬性</li><li>接著 JobDetails.vue export default 的 data() 就可以註解起來</li><li>到網頁畫面一切的功能仍可運作</li><li>在 jobs.vue 新增樣式，Jobs 頁面下方連結樣式</li></ol>



<pre class="wp-block-code"><code>// Jobs.vue - 1 新增 &lt;router-link&gt; 修改內容

&lt;template&gt;
  &lt;h1&gt;Jobs&lt;/h1&gt;
  &lt;div v-for="job in jobs" :key="job.id"&gt;
    &lt;router-link :to="{ name: 'JobDetails', params: { id: job.id } }"&gt;
      &lt;h2&gt;{{ job.title }}&lt;/h2&gt;
    &lt;/router-link&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      jobs: &#91;
        { title: 'Ninja UX Designer', id: 1, details: 'lorem'},
        { title: 'Ninja Web Developer', id: 2, details: 'lorem'},
        { title: 'Ninja Vue Developer', id: 3, details: 'lorem'}
      ]
    }
  }
}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// JobDetails.vue - 3 在 export default 新增 props 屬性

&lt;template&gt;
  &lt;h1&gt;Job Details Page&lt;/h1&gt;
  &lt;p&gt;The job id is {{ id }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'id'],
  data() {
    return {
      id: this.$route.params.id
    }
  }
}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 4 在 routes 的 JobDetails 那個物件新增 props 屬性

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Jobs from '../views/jobs/Jobs.vue'
import JobDetails from '../views/jobs/JobDetails.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  },
  {
    path: '/jobs/:id',
    name: 'JobDetails',
    component: JobDetails,
    props: true
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<pre class="wp-block-code"><code>// JobDetails.vue - 5 data() 註解起來

&lt;template&gt;
  &lt;h1&gt;Job Details Page&lt;/h1&gt;
  &lt;p&gt;The job id is {{ id }}&lt;/p&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: &#91;'id'],
  // data() {
  //   return {
  //     id: this.$route.params.id
  //   }
  // }
}
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre>



<pre class="wp-block-code"><code>// jobs.vue - 7 新增 Jobs 下方連結樣式

&lt;template&gt;
  &lt;h1&gt;Jobs&lt;/h1&gt;
  &lt;div v-for="job in jobs" :key="job.id" class="job"&gt;
    &lt;router-link :to="{ name: 'JobDetails', params: { id: job.id } }"&gt;
      &lt;h2&gt;{{ job.title }}&lt;/h2&gt;
    &lt;/router-link&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      jobs: &#91;
        { title: 'Ninja UX Designer', id: 1, details: 'lorem'},
        { title: 'Ninja Web Developer', id: 2, details: 'lorem'},
        { title: 'Ninja Vue Developer', id: 3, details: 'lorem'}
      ]
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
  .job h2 {
    background: #f4f4f4;
    padding: 20px;
    border-radius: 10px;
    margin: 10px auto;
    max-width: 600px;
    cursor: pointer;
    color: #444;
  }
  .job h2:hover {
    background: #ddd;
  }
  .job a {
    text-decoration: none;
  }
&lt;/style&gt;</code></pre>



<h3 class="wp-block-heading">404 Pages &amp; Redirects</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>在 index.js 新增 redirect</li><li>到網頁瀏覽器網址輸入 all-jobs 會重定向到 jobs</li><li>錯誤的網址輸入會出現錯誤頁面</li><li>在 views 資料夾建立 NotFound.vue 檔案(也可以客製化名稱)，新增樣板內容</li><li>在 src/router/index 新增 catchall 404 內容，新增 import NotFound</li><li>到網頁測試錯誤網址是否能出現錯誤畫面</li></ol>



<pre class="wp-block-code"><code>// index.js - 1 新增 redirect

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Jobs from '../views/jobs/Jobs.vue'
import JobDetails from '../views/jobs/JobDetails.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  },
  {
    path: '/jobs/:id',
    name: 'JobDetails',
    component: JobDetails,
    props: true
  },
  // redirect
  {
    path: '/all-jobs',
    redirect: '/jobs'
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<pre class="wp-block-code"><code>// NotFound.vue - 4 新增樣板內容

&lt;template&gt;
  &lt;h2&gt;404&lt;/h2&gt;
  &lt;h3&gt;Page not found&lt;/h3&gt;
&lt;/template&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 5 新增 catchall 404 內容、新增 import NotFound

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import NotFound from '../views/NotFound.vue'
import Jobs from '../views/jobs/Jobs.vue'
import JobDetails from '../views/jobs/JobDetails.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  },
  {
    path: '/jobs/:id',
    name: 'JobDetails',
    component: JobDetails,
    props: true
  },
  // redirect
  {
    path: '/all-jobs',
    redirect: '/jobs'
  },
  // catchall 404
  {
    path: '/:catchAll(.*)',
    name: 'NotFound',
    component: NotFound
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Programmatic Navigation (程序化導航)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>在 App.vue 新增三個按鈕、@click 事件、&lt;script&gt; export default、button 樣式</li><li>開始撰寫 back() 內容($router 與之前$route 的不同)，並測試向前的功能。接著寫 forward() 內容，然後測試向後的功能。接著寫 redirect() 內容，然後測試重定向到 home 頁面的功能。</li></ol>



<pre class="wp-block-code"><code>// App.vue - 1 新增三個按鈕、@click 事件、&lt;script&gt; export default、button 樣式

&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'About' }"&gt;About&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'Jobs' }"&gt;Jobs&lt;/router-link&gt;
  &lt;/div&gt;

  &lt;button @click="redirect"&gt;Redirect&lt;/button&gt;
  &lt;button @click="back"&gt;Go back&lt;/button&gt;
  &lt;button @click="forward"&gt;Go forward&lt;/button&gt;

  &lt;router-view/&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  methods: {
    redirect() {},
    back() {},
    forward() {}
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 4px;
}

#nav a.router-link-exact-active {
  color: #fff;
  background: crimson;
}

button {
  margin: 0 10px;
  padding: 10px;
  border: none;
  border-radius: 4px;
}
&lt;/style&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue - 2 撰寫 back()、forward()、redirect()

&lt;template&gt;
  &lt;div id="nav"&gt;
    &lt;router-link to="/"&gt;Home&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'About' }"&gt;About&lt;/router-link&gt; |
    &lt;router-link :to="{ name: 'Jobs' }"&gt;Jobs&lt;/router-link&gt;
  &lt;/div&gt;

  &lt;button @click="redirect"&gt;Redirect&lt;/button&gt;
  &lt;button @click="back"&gt;Go back&lt;/button&gt;
  &lt;button @click="forward"&gt;Go forward&lt;/button&gt;

  &lt;router-view/&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  methods: {
    redirect() {
      this.$router.push({ name: 'Home' })
    },
    back() {
      this.$router.go(-1)
    },
    forward() {
      this.$router.go(1)
    }
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 4px;
}

#nav a.router-link-exact-active {
  color: #fff;
  background: crimson;
}

button {
  margin: 0 10px;
  padding: 10px;
  border: none;
  border-radius: 4px;
}
&lt;/style&gt;
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Lazy Loading Components</h3>



<p>稍微簡單提及，可重複觀看、練習，查看文件。</p>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://router.vuejs.org/guide/advanced/lazy-loading.html" target="_blank" rel="noreferrer noopener">Lazy Loading – Vue Router Docs</a></li></ul>



<h4 class="wp-block-heading">操作步驟</h4>



<ol class="wp-block-list"><li>在 index.js 註解 import About、修改 route 的 About component</li></ol>



<pre class="wp-block-code"><code>// index.js - 1 註解 import About、修改 route 的 About component

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
// import About from '../views/About.vue'
import NotFound from '../views/NotFound.vue'
import Jobs from '../views/jobs/Jobs.vue'
import JobDetails from '../views/jobs/JobDetails.vue'

const routes = &#91;
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () =&gt; import('../views/About.vue')
  },
  {
    path: '/jobs',
    name: 'Jobs',
    component: Jobs
  },
  {
    path: '/jobs/:id',
    name: 'JobDetails',
    component: JobDetails,
    props: true
  },
  // redirect
  {
    path: '/all-jobs',
    redirect: '/jobs'
  },
  // catchall 404
  {
    path: '/:catchAll(.*)',
    name: 'NotFound',
    component: NotFound
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Modern JavaScript (4)</title>
		<link>/wordpress_blog/modern-javascript-4/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Tue, 15 Feb 2022 06:17:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=527</guid>

					<description><![CDATA[(Complete guide, from Novice to  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>(Complete guide, from Novice to Ninja)</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Learning Udemy Course：<a href="https://www.udemy.com/course/modern-javascript-from-novice-to-ninja/" target="_blank" rel="noreferrer noopener">Modern JavaScript</a></p><cite>建立者：The Net Ninja (Shaun Pelling)</cite></blockquote>



<p>Learn Modern JavaScript from the very start to ninja-level &amp; build awesome JavaScript applications.</p>



<h4 class="wp-block-heading">您會學到</h4>



<ul class="wp-block-list"><li>Learn how to program with modern JavaScript, from the very beginning to more advanced topics</li><li>Learn all about OOP (object-oriented programming) with JavaScript, working with prototypes &amp; classes</li><li>Learn how to create real-world front-end applications with JavaScript (quizes, weather apps, chat rooms etc)</li><li>Learn how to make useful JavaScript driven UI components like popups, drop-downs, tabs, tool-tips &amp; more.</li><li>Learn how to use modern, cutting-edge JavaScript features today by using a modern workflow (Babel &amp; Webpack)</li><li>Learn how to use real-time databases to store, retrieve and update application data</li><li>Explore API’s to make the most of third-party data (such as weather information)</li></ul>



<h2 class="wp-block-heading">Modern Workflow with Babel &amp; Webpack</h2>



<h3 class="wp-block-heading">Modern Feature Support</h3>



<h4 class="wp-block-heading">MDN 文件</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Classes#%E7%80%8F%E8%A6%BD%E5%99%A8%E7%9B%B8%E5%AE%B9%E6%80%A7" target="_blank">瀏覽器兼容性</a></li><li><a rel="noreferrer noopener" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#browser_compatibility" target="_blank">Browser compatibility</a></li><li><a rel="noreferrer noopener" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#browser_compatibility" target="_blank">Template literals Browser Compatibility</a></li></ul>



<h4 class="wp-block-heading">工具</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://babeljs.io/" target="_blank">Babel</a></li><li><a href="https://webpack.js.org/" target="_blank" rel="noreferrer noopener">webpack</a></li></ul>



<h3 class="wp-block-heading">An Introduction to Babel</h3>



<p>Modern Code 編譯成 以前版本的 Code</p>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://babeljs.io/" target="_blank">Babel Website</a></li></ul>



<h4 class="wp-block-heading">操作</h4>



<ul class="wp-block-list"><li>點擊 Try it out 使用</li></ul>



<p>沒有顯示舊版本的程式碼，Left side menu – ENV PRESET – FORCE ALL TRANSFORMS select.</p>



<pre class="wp-block-code"><code>// Babel 網頁版使用 - 1

function greet() {
  console.log('hello');
}</code></pre>



<pre class="wp-block-code"><code>// Babel 網頁版使用 - 2

const greet = function() {
  console.log('hello');
}</code></pre>



<pre class="wp-block-code"><code>// Babel 網頁版使用 - 3

const greet = function(name){
  console.log(`hello ${name}`);
}</code></pre>



<pre class="wp-block-code"><code>// Babel 網頁版使用 - 4

const greet = (name) =&gt; {
  console.log(`hello ${name}`);
}</code></pre>



<pre class="wp-block-code"><code>// Babel 網頁版使用 - 5

const greet = (name) =&gt; {
  console.log(`hello ${name}`);
}

class User {
  constructor(name){
    this.name = name;
  }
}</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Installing Node.js &amp; Babel</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>使用 Terminal 終端機查詢有無安裝 node.js<ul><li>輸入指令 node -v</li></ul></li><li>沒有就到Node.js官網下載、安裝，有就繼續以下步驟</li><li>清除 Terminal 終端機介面內容 clear</li><li>移動到專案資料夾 – cd 資料夾名稱</li><li>在 Terminal 終端機輸入 npm init 指令初始化項目，這裡沒有要特別設定就全部按 enter 就可以了，完成後會產生 package.json 檔案</li><li>使用 Terminal 終端機安裝 Babel core、Babel cli<ul><li>指令 npm install @babel/core @babel/cli –save-dev</li></ul></li><li>接著再使用 Terminal 終端機安裝 Babel preset-env<ul><li>指令 npm install @babel/preset-env –save-dev</li></ul></li><li>安裝好的套件會在 package.json 檔案呈現安裝的名稱、版本</li><li>建立一個 .babelrc 檔案</li></ul>



<pre class="wp-block-code"><code>// .babelrc

{
  "presets": &#91;"@babel/preset-env"]
}</code></pre>



<h4 class="wp-block-heading">Using Babel</h4>



<ol class="wp-block-list"><li>Install Node.js (and npm) on our computer</li><li>Use npm init to create a package.json file in the project directory</li><li>Use npm to install babel/core &amp; babel/cli packages</li><li>Install the babel preset (env) &amp; register it in .babelrc</li></ol>



<h3 class="wp-block-heading">Using the Babel CLI</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>建立 before.js 檔案</li><li>使用 Terminal 終端機輸入指令<ul><li>node_modules/.bin/babel before.js -o after.js</li></ul></li><li>完成後會產生 after.js 檔案、以及以前版本的程式碼</li><li>移除 node_modules 資料夾</li><li>使用 npm install 指令安裝 node_modules 資料夾</li></ul>



<pre class="wp-block-code"><code>// before.js

const greet = (name) =&gt; {
  console.log(`hello ${name}`);
};

 greet();</code></pre>



<h4 class="wp-block-heading">講解</h4>



<p>可以使用 npm install 安裝 package.json 檔案套件名稱、版本</p>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">NPM Scripts &amp; Watching Files</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>建立一個資料夾 src</li><li>在 src 資料夾裡面建立 index.js 檔案</li><li>建立一個資料夾 dist</li><li>在 dist 資料夾裡面建立 assets 資料夾</li><li>在 assets 資料夾裡面建立 bundle.js</li><li>在 dist 資料夾裡面建立 index.html 檔案</li><li>會在 src/index.js 檔案撰寫 js 程式碼</li><li>在 Terminal 終端機輸入指令<ul><li>node_modules/.bin/babel src/index.js -o dist/assets/bundle.js</li></ul></li><li>完成後 index.js 程式碼就會編譯到 bundle.js</li><li>可以在 package.json 新增 “scripts” 指令</li><li>執行 “scripts” 指令 (因為沒有變動程式碼，所以接著以下步驟)<ul><li>npm run babel</li></ul></li><li>在 index.js 檔案刪除這一行程式碼 greet(‘link’);、然後儲存</li><li>執行 “scripts” 指令<ul><li>npm run babel</li></ul></li><li>Watching Files 需要修改 “scripts” 指令</li></ul>



<pre class="wp-block-code"><code>// dist/index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;script src="assets/bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js

const greet = name =&gt; {
  console.log(`hello ${name}`);
};

greet('mario');
greet('luigi');
greet('link');</code></pre>



<pre class="wp-block-code"><code>// package.json - 1

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "node_modules/.bin/babel src/index.js -o dist/assets/bundle.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11"
  }
}
</code></pre>



<h4 class="wp-block-heading">使用 “scripts” 指令產生錯誤無法執行</h4>



<ul class="wp-block-list"><li>修改成以下程式碼<ul><li>“babel” “babel src/index.js -o dist/assets/bundle.js</li></ul></li></ul>



<pre class="wp-block-code"><code>// package.json - 2

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -o dist/assets/bundle.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// package.json - 3 Watching Files

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// index.js - 3 Watching Files

const greet = name =&gt; {
  console.log(`hello ${name}`);
};

greet('mario');
greet('luigi');
greet('link');

class User {
  constructor(){
    this.score = 0;
  }
}</code></pre>



<h3 class="wp-block-heading">What is Webpack?</h3>



<h4 class="wp-block-heading">Webpack</h4>



<ul class="wp-block-list"><li>Webpack is a&nbsp;<strong>module bundler</strong>(模組打包工具)</li><li>Works well with babel</li><li>Local development server</li></ul>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1399" height="533" src="/wordpress_blog/wp-content/uploads/2022/04/bundle.png" alt="" class="wp-image-529"/><figcaption>bundle</figcaption></figure>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Setting up a Webpack File</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在專案裡建立 webpack.config.js 檔案</li><li>entry 進入點、output 輸出點</li></ul>



<pre class="wp-block-code"><code>// webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  }
};

</code></pre>



<h4 class="wp-block-heading">webpack 文件</h4>



<ul class="wp-block-list"><li><a href="https://webpack.js.org/concepts/" target="_blank" rel="noreferrer noopener">Concepts 連結</a></li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Webpack CLI (Command-Line Interface 命令列介面)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>使用 Terminal 終端機移動到專案位置 cd 專案名稱</li><li>使用 Terminal 終端機輸入指令安裝 webpack、webpack-cli<ul><li>npm install webpack webpack-cli –save-dev</li></ul></li><li>使用 Terminal 終端機輸入指令執行 webpack、在這之前調整 src/index.js 檔案內容，確認 webpack.config.js 有儲存、然後執行以下指令<ul><li>node_modules/.bin/webpack</li></ul></li><li>完成後會發現 dist/assets/bundle.js 檔案有很大的不同，會變成只有一行程式碼</li><li>在 package.json 檔案 “scripts” 新增指令<ul><li>“webpack”: “node_modules/.bin/webpack”</li></ul></li><li>在 index.js 修改程式碼內容後，執行 package.json 裡面的 “scripts” 的 “webpack” 指令<ul><li>執行指令 npm run webpack</li></ul></li></ul>



<pre class="wp-block-code"><code>// index.js - 1

const greet = name =&gt; {
  console.log(`hello ${name}`);
};

greet('mario');
greet('luigi');
greet('link');

</code></pre>



<pre class="wp-block-code"><code>// package.json - 新增 "scripts" 指令 "webpack"

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js",
    "webpack": "node_modules/.bin/webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// index.js - 2

const greet = name =&gt; {
  console.log(`hello ${name}`);
};

greet('mario');
greet('luigi');
greet('link');
greet('link');

</code></pre>



<pre class="wp-block-code"><code>// package.json - 1 產生錯誤

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js",
    "webpack": "node_modules/.bin/webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  }
}
</code></pre>



<h4 class="wp-block-heading">執行 webpack 指令產生錯誤</h4>



<ul class="wp-block-list"><li>package.json 檔案裡面的 “scripts” 指令的 “webpack” 做修改<ul><li>“webpack”: “webpack”</li></ul></li></ul>



<pre class="wp-block-code"><code>// package.json - 2 修改 "webpack" 指令

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js",
    "webpack": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  }
}
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Introduction to Modules</h3>



<p>比較複雜，不懂可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>刪除 src/index.js 裡面所有程式碼</li><li>在 src 資料夾裡面建立 dom.js 檔案</li><li>在 src/index.js 載入 dom.js</li><li>執行 webpack 指令<ul><li>npm run webpack</li></ul></li><li>在 dist/assets/bundle.js 可以看到編譯後的程式碼</li><li>網頁出現錯誤、重新執行 live server，可以看到網頁畫面、以及 console.log 的結果</li><li>用 index.js 講解變數、函式無法從 dom.js 拿來 index.js 用，每個檔案有自己的獨特的範圍</li><li>執行 webpack 指令</li><li>dom.js 新增 export</li><li>index.js 修改 import</li><li>儲存後、執行 npm run webpack 指令，可以看到網頁新增 test 標題</li><li>dom.js 刪除一些程式碼</li><li>index.js 新增一些程式碼</li><li>執行 npm run webpack 指令，這樣仍會運作</li><li>在 dom.js 再新增 export</li><li>在 index.js 再新增 import</li><li>儲存後、執行 npm run webpack 指令</li><li>dom.js 在底下一起 export</li><li>index.js 一樣、不用變動</li><li>執行 npm run webpack 指令、一樣運作</li></ul>



<pre class="wp-block-code"><code>// dom.js - 1

console.log('dom file');

const body = document.querySelector('body');

const styleBody = () =&gt; {
  body.style.background = 'peachpuff';
};

const addTitle = (text) =&gt; {
  const title = document.createElement('h1');
  title.textContent = text;
  body.appendChild(title)
};

styleBody();
addTitle('hello, world from the dom file');</code></pre>



<pre class="wp-block-code"><code>// index.js - 1

import './dom';

console.log('index file');
</code></pre>



<pre class="wp-block-code"><code>// index.js - 2

import './dom';

console.log('index file');
addTitle('test');
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   dom file
   index file
x  Uncaught ReferenceError: addTitle is not defined
&gt;</code></pre>



<pre class="wp-block-code"><code>// dom.js - 3

console.log('dom file');

const body = document.querySelector('body');

export const styleBody = () =&gt; {
  body.style.background = 'peachpuff';
};

export const addTitle = (text) =&gt; {
  const title = document.createElement('h1');
  title.textContent = text;
  body.appendChild(title)
};

styleBody();
addTitle('hello, world from the dom file');</code></pre>



<pre class="wp-block-code"><code>// index.js - 3

import {styleBody, addTitle} from './dom';

console.log('index file');
addTitle('test');
</code></pre>



<pre class="wp-block-code"><code>// dom.js - 4 刪除一些程式碼

console.log('dom file');

const body = document.querySelector('body');

export const styleBody = () =&gt; {
  body.style.background = 'peachpuff';
};

export const addTitle = (text) =&gt; {
  const title = document.createElement('h1');
  title.textContent = text;
  body.appendChild(title)
};

</code></pre>



<pre class="wp-block-code"><code>// index.js - 4 新增一些程式碼

import {styleBody, addTitle} from './dom';

console.log('index file');
addTitle('test');
styleBody();</code></pre>



<pre class="wp-block-code"><code>// dom.js - 5 再新增 export

console.log('dom file');

const body = document.querySelector('body');

export const styleBody = () =&gt; {
  body.style.background = 'peachpuff';
};

export const addTitle = (text) =&gt; {
  const title = document.createElement('h1');
  title.textContent = text;
  body.appendChild(title)
};

export const contact = 'mario@thenetninja.co.uk';
</code></pre>



<pre class="wp-block-code"><code>// index.js - 5 再新增 import

import {styleBody, addTitle, contact} from './dom';

console.log('index file');
addTitle('test');
styleBody();
console.log(contact);</code></pre>



<pre class="wp-block-code"><code>// dom.js - 6 在底下一起 export

console.log('dom file');

const body = document.querySelector('body');

const styleBody = () =&gt; {
  body.style.background = 'peachpuff';
};

const addTitle = (text) =&gt; {
  const title = document.createElement('h1');
  title.textContent = text;
  body.appendChild(title)
};

const contact = 'mario@thenetninja.co.uk';

export { styleBody, addTitle, contact };</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Default Exports</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 src 資料夾建立 data.js 檔案</li><li>在 data.js 檔案新增程式碼內容</li><li>在 index.js 檔案 import 載入 data.js</li><li>執行 webpack 指令</li><li>查看 Google Console</li><li>在 data.js 新增函式</li><li>在 index.js 程式碼新增</li><li>儲存後、執行 webpack 指令</li><li>查看 Google Console</li><li>在 data.js 另一種 export 寫法、其他不變</li><li>儲存後、執行 webpack 指令，所有仍然運作</li></ul>



<pre class="wp-block-code"><code>// data.js - 1

const users = &#91;
  { name: 'mario', premium: true },
  { name: 'luigi', premium: false },
  { name: 'yoshi', premium: true },
  { name: 'toad', premium: true },
  { name: 'peach', premium: false }
];

export default users;</code></pre>



<pre class="wp-block-code"><code>// index.js - 1

import {styleBody, addTitle, contact} from './dom';
import users from './data';

console.log(users);

</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   dom file
   (5) &#91;{...}, {...}, {...}, {...}, {...}]
&gt;</code></pre>



<pre class="wp-block-code"><code>// data.js - 2 新增函式

const users = &#91;
  { name: 'mario', premium: true },
  { name: 'luigi', premium: false },
  { name: 'yoshi', premium: true },
  { name: 'toad', premium: true },
  { name: 'peach', premium: false }
];

export const getPremUsers = (users) =&gt; {
  return users.filter(user =&gt; user.premium);
};

export default users;</code></pre>



<pre class="wp-block-code"><code>// index.js - 2

import {styleBody, addTitle, contact} from './dom';
import users, { getPremUsers } from './data';

const premUsers = getPremUsers(users);
console.log(users, premUsers);

</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   dom file
   (5) &#91;{...}, {...}, {...}, {...}, {...}] (3) &#91;{...}, {...}, {...}]
&gt;</code></pre>



<pre class="wp-block-code"><code>// data.js - 3 另一種 export 寫法

const users = &#91;
  { name: 'mario', premium: true },
  { name: 'luigi', premium: false },
  { name: 'yoshi', premium: true },
  { name: 'toad', premium: true },
  { name: 'peach', premium: false }
];

const getPremUsers = (users) =&gt; {
  return users.filter(user =&gt; user.premium);
};

export { getPremUsers, users as default }</code></pre>



<h3 class="wp-block-heading">Watching for Changes</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 package.json 修改 “scripts” 中 “webpack” 指令<ul><li>修改前 “node_modules/.bin/webpack -w”</li><li>修改後 “webpack”: “webpack -w”</li></ul></li><li>用 index.js 檔案測試 watching 是否有正確使用</li><li>查看 Google Console 看是否有執行</li><li>停止 watching 可以在 Terminal 終端機使用 Ctrl + c、然後輸入 Y 停止</li></ul>



<pre class="wp-block-code"><code>// package.json

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js",
    "webpack": "webpack -w"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// index.js

import {styleBody, addTitle, contact} from './dom';
import users, { getPremUsers } from './data';

const premUsers = getPremUsers(users);
console.log(users, premUsers);

console.log('test');</code></pre>



<pre class="wp-block-code"><code>// Google Console
   dom file
   (5) &#91;{...}, {...}, {...}, {...}, {...}] (3) &#91;{...}, {...}, {...}]
   test
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">The Webpack Dev Server</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>停止 live server</li><li>使用 Terminal 終端機安裝套件<ul><li>產生錯誤 npm install webpack-dev-server@3.2.1</li><li>修正後 npm install webpack-dev-server 版本是 4.7.4</li></ul></li><li>在 webpack.config.js 新增關於 devServer 程式碼</li><li>在 package.json 新增 “scripts” 指令<ul><li>“serve”: “webpack-dev-server”</li></ul></li><li>執行 serve 指令、產生錯誤</li><li>修改 webpack.config.js 的 devServer</li><li>在終端機 Project is running at 連結，使用 ctrl+左鍵打開網頁</li><li>用 index.js 來測試是否又正確執行，結果出現 test 沒有變動</li><li>再次修改 webpack.config.js 的 devServer，test 會變動了</li><li>有一個問題是虛擬檔案、沒有編譯程式碼到 bundle.js</li><li>藉由刪除 bundle.js 所有程式碼來展示確實沒有編譯程式碼到 bundle.js</li><li>用 index.js 來測試，bundle.js 一樣沒有編譯完成的程式碼</li></ul>



<h4 class="wp-block-heading">webpack 文件</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://webpack.js.org/api/webpack-dev-server/" target="_blank">webpack-dev-server API</a></li><li><a href="https://webpack.js.org/configuration/dev-server/" target="_blank" rel="noreferrer noopener">DevServer</a></li></ul>



<pre class="wp-block-code"><code>// package.json

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js",
    "webpack": "webpack -w"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  },
  "dependencies": {
    "webpack-dev-server": "^4.7.4"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// webpack.config.js - 修改前

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    publicPath: '/assets/'
  }
};

</code></pre>



<pre class="wp-block-code"><code>// package.json - 修改前

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src/index.js -w -o dist/assets/bundle.js",
    "webpack": "webpack -w",
    "serve": "webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  },
  "dependencies": {
    "webpack-dev-server": "^4.7.4"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// webpack.config.js - 修改後

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    }
  }
};

</code></pre>



<pre class="wp-block-code"><code>// index.js - 測試

import {styleBody, addTitle, contact} from './dom';
import users, { getPremUsers } from './data';

const premUsers = getPremUsers(users);
console.log(users, premUsers);

console.log('test 2');</code></pre>



<pre class="wp-block-code"><code>// webpack.config.js - 再次修改 devServer

const path = require('path');

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist')
    },
    devMiddleware: {
      publicPath: '/assets'
    }
  },
};

</code></pre>



<pre class="wp-block-code"><code>// index.js - 再測試

import {styleBody, addTitle, contact} from './dom';
import users, { getPremUsers } from './data';

const premUsers = getPremUsers(users);
console.log(users, premUsers);

console.log('test 3');</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Production &amp; Development Modes</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>修改 package.json 的 “scripts” 指令<ul><li>移除 “babel” 指令</li><li>修改 “webpack” 指令改成 “build” 指令</li></ul></li><li>停止 Terminal 終端機 Ctrl + c</li><li>執行指令 npm run build，就會看到 bundle.js 編譯出程式碼</li><li>終端機出現警告是關於沒有設定 mode 的訊息</li><li>在 package.json “scripts” 指令 “build”、”serve” 設定 mode<ul><li>“build”: “webpack –mode production”</li><li>“serve”: “webpack-dev-server –mode development”</li></ul></li><li>在終端機先使用 clear 清楚終端機畫面，執行 npm run build 指令</li><li>執行 npm run serve 指令</li></ul>



<pre class="wp-block-code"><code>// package.json - 1

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "serve": "webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  },
  "dependencies": {
    "webpack-dev-server": "^4.7.4"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// webpack.config.js - 把 mode 移除

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist')
    },
    devMiddleware: {
      publicPath: '/assets'
    }
  },
};

</code></pre>



<pre class="wp-block-code"><code>// package.json - 2 mode 設定

{
  "name": "modern-javascript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode production",
    "serve": "webpack-dev-server --mode development"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.0",
    "@babel/preset-env": "^7.16.11",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  },
  "dependencies": {
    "webpack-dev-server": "^4.7.4"
  }
}
</code></pre>



<h4 class="wp-block-heading">webpack 指令模式</h4>



<ul class="wp-block-list"><li>production mode 上線模式</li><li>development mode 開發模式</li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Babel &amp; Webpack Together</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>安裝 babel-loader 套件<ul><li>終端機指令 npm install babel-loader –save-dev</li></ul></li><li>在 webpack.config.js 新增 module 程式碼</li><li>在 data.js 我們有使用箭頭函式</li><li>在 Terminal 終端機輸入指令<ul><li>npm run build</li></ul></li><li>查看 bundle.js 是否有編譯成以前的程式碼</li></ul>



<pre class="wp-block-code"><code>// webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist')
    },
    devMiddleware: {
      publicPath: '/assets'
    }
  },
  module: {
    rules: &#91;{
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: &#91;'@babel/preset-env']
        }
      }
    }]
  }
};

</code></pre>



<h3 class="wp-block-heading">Webpack Boilerplate (Webpack 樣板)</h3>



<p>可以下載 Webpack Boilerplate (Webpack 樣板) 使用。</p>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://github.com/iamshaunjp/modern-javascript/tree/lesson-165" target="_blank" rel="noreferrer noopener">Link to Boilerplate Webpack Template on GitHub</a></li></ul>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>下載 Webpack Boilerplate</li><li>複製 Boilerplate 裡面的檔案，貼到自己的 project 專案</li><li>可以藉由 package.json 檔案裡面的套件名稱、版本安裝 node_modules 資料夾<ul><li>終端機指令 npm install</li></ul></li><li>在 package.json 找到 “scripts” 指令，並執行 “serve” 指令<ul><li>npm run serve</li></ul></li><li>打開終端機出現的本地端網址，在 Google Console 上並沒有出現任何程式碼</li><li>在 index.js 撰寫程式碼</li><li>查看 Google Console 是否有出現 console 結果</li></ul>



<pre class="wp-block-code"><code>// index.js

console.log('hello, ninjas');</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Project – UI Library</h2>



<p>比較複雜，可重複觀看、練習。</p>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Project Preview &amp; Setup</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>下載 Webpack Boilerplate</li><li>建立一個資料夾 ui-lib-project</li><li>複製 Webpack Boilerplate 裡面的檔案到 ui-lib-project 資料夾</li><li>打開 Terminal 終端機移動到專案位置 cd ui-lib-project</li><li>安裝 package.json 裡面的所有套件<ul><li>指令 npm install</li></ul></li><li>查看 package.json 上的 “scripts” 指令中的 “serve” 指令<ul><li>執行指令 npm run serve</li></ul></li><li>開啟終端機上的本地端網址</li><li>藉由 index.js 測試是否有正常運行</li><li>查看 Google Console 有無出現 console 結果</li><li>在 src 資料夾裡面建立 ninja-ui 資料夾</li></ul>



<pre class="wp-block-code"><code>// index.js

console.log('test');</code></pre>



<h4 class="wp-block-heading">UI Library CSS</h4>



<p>Keep the library css in JavaScript together inside the folder right here. And that means we’re going to need to process our css through webpack using css loader, because this is in the source folder, not the distribution folder.</p>



<p>在下一章節會講到如何透過 Webpack 處理 CSS。</p>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">CSS &amp; Style Loaders (webpack)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>停止終端機 Ctrl + C</li><li>安裝 css-loader、style-loader 套件<ul><li>終端機指令 npm install css-loader style-loader –save-dev</li></ul></li><li>在 webpack.config.js module 的 rules 裡面程式碼新增第二個</li><li>執行 “scripts” 指令 npm run serve</li><li>在 src 資料夾新增一個 test.css 檔案</li><li>使用終端機打開本地端網頁，沒有產生渲染結果，因為我們沒有 import 這個 css 檔案</li><li>在 index.js import 載入 test.css</li><li>查看 Goolge Elemets 在 &lt;head&gt; 可以看到 CSS 樣式</li><li>在 ninja_ui 資料夾建立一個 styles 資料夾</li></ul>



<pre class="wp-block-code"><code>// webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/assets'),
    filename: 'bundle.js'
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist')
    },
    devMiddleware: {
      publicPath: '/assets'
    }
  },
  module: {
    rules: &#91;
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: &#91;'@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: &#91;'style-loader', 'css-loader']
      }
    ]
  }
};

</code></pre>



<pre class="wp-block-code"><code>// test.css

body {
  background: orange;
}</code></pre>



<pre class="wp-block-code"><code>// index.js

import './test.css';

console.log('test');</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Tooltips (工具提示框)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增程式碼內容、樣式</li><li>在 ninja-ui 資料夾建立 tooltip.js 檔案</li><li>在 index.js import 載入 Tooltip、並 create a tooltip，在那之前要先在 tooltip.js export 匯出</li><li>在 styles 資料夾建立 tooltip.css 檔案</li><li>在 tooltip.js import 載入 tooltip.css</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;UI Library&lt;/title&gt;
  &lt;style&gt;
    body {
      font-size: 1.5em;
    }

    .container {
      margin: 40px;
      margin-top: 60px;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;div class="container"&gt;
    &lt;!-- tooltop --&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, reiciendis vel mollitia neque libero nulla, earum, nisi a distinctio doloremque porro aliquam. Alias, ratione. Laudantium neque enim et ipsa corrupti. &lt;span class="tooltip" data-message="I'm a tooltip!!"&gt;Lorem, ipsum.&lt;/span&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quasi sequi natus pariatur odio repudiandae provident. Accusamus quidem quaerat, enim beatae dolorum iusto in iure similique esse tempora animi, non eveniet!&lt;/p&gt;
  &lt;/div&gt;

  &lt;script src="assets/bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// tooltip.js - 1

class Tooltip {
  constructor(element){
    this.element = element;
    this.message = element.getAttribute('data-message');
  }
  init(){
    const tip = document.createElement('div');
    tip.classList.add('tip');
    tip.textContent = this.message;
    this.element.appendChild(tip);
  }
}</code></pre>



<pre class="wp-block-code"><code>// tooltip.js - 2 export 匯出

class Tooltip {
  constructor(element){
    this.element = element;
    this.message = element.getAttribute('data-message');
  }
  init(){
    const tip = document.createElement('div');
    tip.classList.add('tip');
    tip.textContent = this.message;
    this.element.appendChild(tip);
  }
}

export { Tooltip as default };</code></pre>



<pre class="wp-block-code"><code>// index.js

import Tooltip from './ninja-ui/tooltip';

// create a tooltip
const tooltip = new Tooltip(document.querySelector('.tooltip'));

tooltip.init();
</code></pre>



<pre class="wp-block-code"><code>// tooltip.js - 3 addEventListener

class Tooltip {
  constructor(element){
    this.element = element;
    this.message = element.getAttribute('data-message');
  }
  init(){
    const tip = document.createElement('div');
    tip.classList.add('tip');
    tip.textContent = this.message;
    this.element.appendChild(tip);

    this.element.addEventListener('mouseenter', () =&gt; {
      tip.classList.add('active');
    });

    this.element.addEventListener('mouseleave', () =&gt; {
      tip.classList.remove('active');
    });
  }
}

export { Tooltip as default };</code></pre>



<pre class="wp-block-code"><code>// styles/tooltip.css

.tooltip {
  position: relative;
  display: inline-block;
  color: #ff6565;
  border-bottom: 1px dotted #ff6565;
  cursor: help;
}
.tip {
  visibility: hidden;
  width: 150px;
  background-color: #ff6565;
  color: #fff;
  text-align: center;
  border-radius: 10px;
  padding: 5px 0;
  position: absolute;
  bottom: 120%;
  left: 50%;
  margin-left: -75px;
  opacity: 0;
  transition: opacity 0.3s;
}
.tip.active {
  visibility: visible;
  opacity: 1;
}
.tip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -4px;
  border-width: 4px;
  border-style: solid;
  border-color: transparent;
  border-top-color: #ff6565;
}</code></pre>



<pre class="wp-block-code"><code>// tooltip.js - 4

import './styles/tooltip.css';

class Tooltip {
  constructor(element){
    this.element = element;
    this.message = element.getAttribute('data-message');
  }
  init(){
    const tip = document.createElement('div');
    tip.classList.add('tip');
    tip.textContent = this.message;
    this.element.appendChild(tip);

    this.element.addEventListener('mouseenter', () =&gt; {
      tip.classList.add('active');
    });

    this.element.addEventListener('mouseleave', () =&gt; {
      tip.classList.remove('active');
    });
  }
}

export { Tooltip as default };</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Dropdowns</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增程式碼內容</li><li>在 ninja-ui 資料夾建立 dropdown.js 檔案</li><li>在 index.js import 載入 Dropdown、並 create dropdowns</li><li>使用 Google 檢查、藉由點擊觸發監聽行為</li><li>在 styles 資料夾建立 dropdown.css 檔案、新增程式碼內容</li><li>在 dropdown.js import 載入 dropdown.css</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;UI Library&lt;/title&gt;
  &lt;style&gt;
    body {
      font-size: 1.5em;
    }

    .container {
      margin: 40px;
      margin-top: 60px;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;div class="container"&gt;
    &lt;!-- tooltop --&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, reiciendis vel mollitia neque libero nulla, earum, nisi a distinctio doloremque porro aliquam. Alias, ratione. Laudantium neque enim et ipsa corrupti. &lt;span class="tooltip" data-message="I'm a tooltip!!"&gt;Lorem, ipsum.&lt;/span&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quasi sequi natus pariatur odio repudiandae provident. Accusamus quidem quaerat, enim beatae dolorum iusto in iure similique esse tempora animi, non eveniet!&lt;/p&gt;
    &lt;h2&gt;Services&lt;/h2&gt;
    &lt;!-- dropdowns --&gt;
    &lt;div class="dropdown"&gt;
      &lt;div class="trigger"&gt;Awesome T-shirt Designs&lt;/div&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi minus non sed assumenda molestiae amet quisquam temporibus ratione dolorem. Ea non quibusdam eveniet eum tempora molestiae asperiores labore, dolorum iste.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="dropdown"&gt;
      &lt;div class="trigger"&gt;Cool Sticker Printing&lt;/div&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Culpa quidem nihil nam quasi consequatur deserunt nemo alias corrupti ex at nisi fugit veritatis veniam voluptate error recusandae, dolorem debitis unde.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;script src="assets/bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// dropdown.js

class Dropdown {
  constructor(container){
    this.container = container;
    this.trigger = container.querySelector('.trigger');
    this.content = container.querySelector('.content');
  }
  init(){
    this.trigger.addEventListener('click', () =&gt; {
      this.trigger.classList.toggle('active');
      this.content.classList.toggle('active');
    })
  }
}

export { Dropdown as default };</code></pre>



<pre class="wp-block-code"><code>// index.js

import Tooltip from './ninja-ui/tooltip';
import Dropdown from './ninja-ui/dropdown';

// create a tooltip
const tooltip = new Tooltip(document.querySelector('.tooltip'));

tooltip.init();

// create dropdowns
const dropdowns = document.querySelectorAll('.dropdown');

dropdowns.forEach(dropdown =&gt; {
  const instance = new Dropdown(dropdown);
  instance.init();
})</code></pre>



<pre class="wp-block-code"><code>// dropdown.css

.dropdown .trigger{
  border-bottom: 1px solid #ddd;
  padding: 10px;
  position: relative;
  cursor: pointer;
}
.dropdown .trigger::after{
  content: "&gt;";
  display: inline-block;
  position: absolute;
  right: 15px;
  transform: rotate(90deg) scale(0.5, 1);
  font-weight: bold;
  transition: transform 0.3s;
}
.dropdown .trigger.active::after{
  transform: rotate(-90deg) scale(0.5, 1);
}
.dropdown .content{
  display: none;
}
.dropdown .content.active{
  display: block;
}</code></pre>



<pre class="wp-block-code"><code>// dropdown.js import 載入 dropdown.css

import './styles/dropdown.css';

class Dropdown {
  constructor(container){
    this.container = container;
    this.trigger = container.querySelector('.trigger');
    this.content = container.querySelector('.content');
  }
  init(){
    this.trigger.addEventListener('click', () =&gt; {
      this.trigger.classList.toggle('active');
      this.content.classList.toggle('active');
    })
  }
}

export { Dropdown as default };</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Tabbed Content (頁籤內容)</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>新增 index.html Tabbed Content 內容</li><li>使用終端機執行指令 npm run serve 看網頁是否有新增 Tabbed Content 內容</li><li>在 ninja-ui 資料夾建立 tabs.js 檔案，新增 tabs.js 程式碼、export 預設匯出 Tabs</li><li>在 index.js import 載入 Tabs 從 tabs.js，並在下面 create tabs</li><li>在 Google Console 出現錯誤，在 tabs.js 的地方、我們必需把 querySelector 改成 querySelectorAll</li><li>在 Google Elements 測試點擊頁籤看是否會切換 active</li><li>在 styles 資料夾新增 tabs.css 檔案，並新增 CSS 樣式</li><li>在 tabs.js import 載入 tabs.css</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;UI Library&lt;/title&gt;
  &lt;style&gt;
    body {
      font-size: 1.5em;
    }

    .container {
      margin: 40px;
      margin-top: 60px;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;div class="container"&gt;
    &lt;!-- tooltop --&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, reiciendis vel mollitia neque libero nulla, earum, nisi a distinctio doloremque porro aliquam. Alias, ratione. Laudantium neque enim et ipsa corrupti. &lt;span class="tooltip" data-message="I'm a tooltip!!"&gt;Lorem, ipsum.&lt;/span&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quasi sequi natus pariatur odio repudiandae provident. Accusamus quidem quaerat, enim beatae dolorum iusto in iure similique esse tempora animi, non eveniet!&lt;/p&gt;
    &lt;h2&gt;Services&lt;/h2&gt;
    &lt;!-- dropdowns --&gt;
    &lt;div class="dropdown"&gt;
      &lt;div class="trigger"&gt;Awesome T-shirt Designs&lt;/div&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi minus non sed assumenda molestiae amet quisquam temporibus ratione dolorem. Ea non quibusdam eveniet eum tempora molestiae asperiores labore, dolorum iste.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="dropdown"&gt;
      &lt;div class="trigger"&gt;Cool Sticker Printing&lt;/div&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Culpa quidem nihil nam quasi consequatur deserunt nemo alias corrupti ex at nisi fugit veritatis veniam voluptate error recusandae, dolorem debitis unde.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;h2&gt;More information&lt;/h2&gt;
    &lt;!-- tabs --&gt;
    &lt;div class="tabs"&gt;
      &lt;ul&gt;
        &lt;li class="trigger active" data-target="#about"&gt;About&lt;/li&gt;
        &lt;li class="trigger" data-target="#delivery"&gt;Delivery info&lt;/li&gt;
        &lt;li class="trigger" data-target="#returns"&gt;Returns info&lt;/li&gt;
      &lt;/ul&gt;
      &lt;div id="about" class="content active"&gt;
        &lt;h3&gt;about&lt;/h3&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Explicabo natus sequi nemo sint error cupiditate praesentium nisi commodi, beatae laborum quisquam ea, atque recusandae totam optio suscipit, ab corrupti illo.&lt;/p&gt;
      &lt;/div&gt;
      &lt;div id="delivery" class="content"&gt;
        &lt;h3&gt;delivery&lt;/h3&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore autem quisquam architecto aliquam. Corporis veniam recusandae doloribus, vitae illum voluptate earum impedit molestiae deleniti voluptatum inventore voluptates! Delectus, quia excepturi?&lt;/p&gt;
      &lt;/div&gt;
      &lt;div id="returns" class="content"&gt;
        &lt;h3&gt;returns&lt;/h3&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Assumenda, ullam aut! Saepe, pariatur! Est aperiam provident dicta cum. Dolorum doloremque error id! Veniam aut architecto necessitatibus reiciendis officiis quisquam delectus!&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;script src="assets/bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// tabs.js

class Tabs{
  constructor(container){
    this.container = container;
    this.tabs = container.querySelector('.trigger');
  }
  init(){
    this.tabs.forEach(tab =&gt; {
      tab.addEventListener('click', e =&gt; {
        this.toggleTabs(e);
        this.toggleContent(e);
      })
    })
  }
  toggleTabs(e){
    // remove current active classes
    this.tabs.forEach(tab =&gt; tab.classList.remove('active'));
    // add new active class to clicked tab
    e.target.classList.add('active'); 
  }
  toggleContent(e){
    // remove current active classes from content
    this.container.querySelectorAll('.content').forEach(item =&gt; {
      item.classList.remove('active');
    });
    // add new active class to content
    const selector = e.target.getAttribute('data-target');
    const content = this.container.querySelector(selector);
    content.classList.add('active');
  }
}

export { Tabs as default };</code></pre>



<pre class="wp-block-code"><code>// index.js

import Tooltip from './ninja-ui/tooltip';
import Dropdown from './ninja-ui/dropdown';
import Tabs from './ninja-ui/tabs';

// create a tooltip
const tooltip = new Tooltip(document.querySelector('.tooltip'));

tooltip.init();

// create dropdowns
const dropdowns = document.querySelectorAll('.dropdown');

dropdowns.forEach(dropdown =&gt; {
  const instance = new Dropdown(dropdown);
  instance.init();
});

// create tabs
const tabs = new Tabs(document.querySelector('.tabs'));
tabs.init();</code></pre>



<pre class="wp-block-code"><code>// tabs.js - 把第4行 querySelector 改成 querySelectorAll

class Tabs{
  constructor(container){
    this.container = container;
    this.tabs = container.querySelectorAll('.trigger');
  }
  init(){
    this.tabs.forEach(tab =&gt; {
      tab.addEventListener('click', e =&gt; {
        this.toggleTabs(e);
        this.toggleContent(e);
      })
    })
  }
  toggleTabs(e){
    // remove current active classes
    this.tabs.forEach(tab =&gt; tab.classList.remove('active'));
    // add new active class to clicked tab
    e.target.classList.add('active'); 
  }
  toggleContent(e){
    // remove current active classes from content
    this.container.querySelectorAll('.content').forEach(item =&gt; {
      item.classList.remove('active');
    });
    // add new active class to content
    const selector = e.target.getAttribute('data-target');
    const content = this.container.querySelector(selector);
    content.classList.add('active');
  }
}

export { Tabs as default };</code></pre>



<pre class="wp-block-code"><code>// tabs.css

.tabs &gt; ul {
  padding: 0;
}
.tabs .trigger {
  list-style-type: none;
  padding: 10px;
  background: #f2f2f2;
  margin: 4px;
  border-radius: 6px;
  display: inline-block;
  padding: 10px 20px;
  cursor: pointer;
}
.tabs .trigger.active {
  background: #ff6565;
  color: white;
}
.tabs .content {
  background: #fbfbfb;
  padding: 10px 20px;
  border-radius: 6px;
  display: none;
}
.tabs .content.active {
  display: block;
}</code></pre>



<pre class="wp-block-code"><code>// tabs.js

import './styles/tabs.css';

class Tabs{
  constructor(container){
    this.container = container;
    this.tabs = container.querySelectorAll('.trigger');
  }
  init(){
    this.tabs.forEach(tab =&gt; {
      tab.addEventListener('click', e =&gt; {
        this.toggleTabs(e);
        this.toggleContent(e);
      })
    })
  }
  toggleTabs(e){
    // remove current active classes
    this.tabs.forEach(tab =&gt; tab.classList.remove('active'));
    // add new active class to clicked tab
    e.target.classList.add('active'); 
  }
  toggleContent(e){
    // remove current active classes from content
    this.container.querySelectorAll('.content').forEach(item =&gt; {
      item.classList.remove('active');
    });
    // add new active class to content
    const selector = e.target.getAttribute('data-target');
    const content = this.container.querySelector(selector);
    content.classList.add('active');
  }
}

export { Tabs as default };</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Snackbars</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增 snackbars 內容、以及按鈕的 CSS 樣式</li><li>在 ninja-ui 資料夾建立 snackbar.js 檔案，新增 Snackbar 程式碼內容、以及 export 預設載入 Snackbar</li><li>在 index.js import 匯入 Snackbar 從 snackbar.js，並在下面 create snackbar</li><li>在 Google Elements 可在 &lt;body&gt; 之前看到 snackbar 類別</li><li>在 snackbar.js 新增 show() 方法</li><li>在 index.js 新增關於 button addEventListener 按鈕監聽</li><li>在網頁點擊按鈕後會出現文字</li><li>在 styles 資料夾建立 snackbar.css 檔案、並新增 CSS 內容</li><li>在 snackbar.js import 載入 snackbar.css</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;UI Library&lt;/title&gt;
  &lt;style&gt;
    body {
      font-size: 1.5em;
    }

    .container {
      margin: 40px;
      margin-top: 60px;
    }

    button {
      width: 200px;
      background: #eee;
      padding: 10px;
      margin: 20px auto;
      border: none;
      border-radius: 4px;
      font-size: 1em;
      cursor: pointer;
      display: block;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;div class="container"&gt;
    &lt;!-- tooltop --&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic, reiciendis vel mollitia neque libero nulla, earum, nisi a distinctio doloremque porro aliquam. Alias, ratione. Laudantium neque enim et ipsa corrupti. &lt;span class="tooltip" data-message="I'm a tooltip!!"&gt;Lorem, ipsum.&lt;/span&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quasi sequi natus pariatur odio repudiandae provident. Accusamus quidem quaerat, enim beatae dolorum iusto in iure similique esse tempora animi, non eveniet!&lt;/p&gt;
    &lt;h2&gt;Services&lt;/h2&gt;
    &lt;!-- dropdowns --&gt;
    &lt;div class="dropdown"&gt;
      &lt;div class="trigger"&gt;Awesome T-shirt Designs&lt;/div&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi minus non sed assumenda molestiae amet quisquam temporibus ratione dolorem. Ea non quibusdam eveniet eum tempora molestiae asperiores labore, dolorum iste.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="dropdown"&gt;
      &lt;div class="trigger"&gt;Cool Sticker Printing&lt;/div&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Culpa quidem nihil nam quasi consequatur deserunt nemo alias corrupti ex at nisi fugit veritatis veniam voluptate error recusandae, dolorem debitis unde.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;h2&gt;More information&lt;/h2&gt;
    &lt;!-- tabs --&gt;
    &lt;div class="tabs"&gt;
      &lt;ul&gt;
        &lt;li class="trigger active" data-target="#about"&gt;About&lt;/li&gt;
        &lt;li class="trigger" data-target="#delivery"&gt;Delivery info&lt;/li&gt;
        &lt;li class="trigger" data-target="#returns"&gt;Returns info&lt;/li&gt;
      &lt;/ul&gt;
      &lt;div id="about" class="content active"&gt;
        &lt;h3&gt;about&lt;/h3&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Explicabo natus sequi nemo sint error cupiditate praesentium nisi commodi, beatae laborum quisquam ea, atque recusandae totam optio suscipit, ab corrupti illo.&lt;/p&gt;
      &lt;/div&gt;
      &lt;div id="delivery" class="content"&gt;
        &lt;h3&gt;delivery&lt;/h3&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore autem quisquam architecto aliquam. Corporis veniam recusandae doloribus, vitae illum voluptate earum impedit molestiae deleniti voluptatum inventore voluptates! Delectus, quia excepturi?&lt;/p&gt;
      &lt;/div&gt;
      &lt;div id="returns" class="content"&gt;
        &lt;h3&gt;returns&lt;/h3&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Assumenda, ullam aut! Saepe, pariatur! Est aperiam provident dicta cum. Dolorum doloremque error id! Veniam aut architecto necessitatibus reiciendis officiis quisquam delectus!&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div&gt;
      &lt;button class="snackbar-trigger"&gt;click me&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;script src="assets/bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// snackbar.js

class Snackbar {
  constructor(){
    this.snackbar = document.createElement('div');
  }
  init(){
    this.snackbar.classList.add('snackbar');
    document.querySelector('body').appendChild(this.snackbar);
  }
}

export { Snackbar as default };</code></pre>



<pre class="wp-block-code"><code>// index.js

import Tooltip from './ninja-ui/tooltip';
import Dropdown from './ninja-ui/dropdown';
import Tabs from './ninja-ui/tabs';
import Snackbar from './ninja-ui/snackbar';

// create a tooltip
const tooltip = new Tooltip(document.querySelector('.tooltip'));

tooltip.init();

// create dropdowns
const dropdowns = document.querySelectorAll('.dropdown');

dropdowns.forEach(dropdown =&gt; {
  const instance = new Dropdown(dropdown);
  instance.init();
});

// create tabs
const tabs = new Tabs(document.querySelector('.tabs'));
tabs.init();

// create snackbar
const snackbar = new Snackbar();
snackbar.init();</code></pre>



<pre class="wp-block-code"><code>// snackbar.js - show()

class Snackbar {
  constructor(){
    this.snackbar = document.createElement('div');
  }
  init(){
    this.snackbar.classList.add('snackbar');
    document.querySelector('body').appendChild(this.snackbar);
  }
  show(message){
    this.snackbar.textContent = message;
    this.snackbar.classList.add('active');
    setTimeout(() =&gt; {
      this.snackbar.classList.remove('active');
    }, 4000);
  }
}

export { Snackbar as default };</code></pre>



<pre class="wp-block-code"><code>// index.js - button addEventListener

import Tooltip from './ninja-ui/tooltip';
import Dropdown from './ninja-ui/dropdown';
import Tabs from './ninja-ui/tabs';
import Snackbar from './ninja-ui/snackbar';

// create a tooltip
const tooltip = new Tooltip(document.querySelector('.tooltip'));

tooltip.init();

// create dropdowns
const dropdowns = document.querySelectorAll('.dropdown');

dropdowns.forEach(dropdown =&gt; {
  const instance = new Dropdown(dropdown);
  instance.init();
});

// create tabs
const tabs = new Tabs(document.querySelector('.tabs'));
tabs.init();

// create snackbar
const snackbar = new Snackbar();
snackbar.init();

const button = document.querySelector('button');
button.addEventListener('click', () =&gt; {
  snackbar.show('you clicked me :)');
});</code></pre>



<pre class="wp-block-code"><code>// styles/snackbar.css

.snackbar {
  width: 200px;
  padding: 20px;
  position: fixed;
  left: 50%;
  margin-left: -120px;
  top: 0;
  border-radius: 0 0 5px 5px;
  box-shadow: 1px 3px 5px rgba(0,0,0,0.2);
  background: #ff6565;
  text-align: center;
  color: #fff;
  margin-top: -100%;
  transition: all 0.2s;
}
.snackbar.active {
  margin-top: 0;
}</code></pre>



<pre class="wp-block-code"><code>// snackbar.js - import 載入 snackbar.css

import './styles/snackbar.css';

class Snackbar {
  constructor(){
    this.snackbar = document.createElement('div');
  }
  init(){
    this.snackbar.classList.add('snackbar');
    document.querySelector('body').appendChild(this.snackbar);
  }
  show(message){
    this.snackbar.textContent = message;
    this.snackbar.classList.add('active');
    setTimeout(() =&gt; {
      this.snackbar.classList.remove('active');
    }, 4000);
  }
}

export { Snackbar as default };</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Extending the Library</h3>



<h4 class="wp-block-heading">build and upload our final project (建立以及上傳你最終專案)</h4>



<ul class="wp-block-list"><li>在 package.json 查看 “scripts” 指令</li><li>執行 “build” 指令 npm run build</li></ul>



<h4 class="wp-block-heading">延伸使用 Library</h4>



<ul class="wp-block-list"><li>ninja-ui 資料夾可以複製到不同專案上使用</li></ul>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Using Firebase Database (&amp; Auth) Version 9</h2>



<p>更新之前 Databases (Firebase) 章節。</p>



<h3 class="wp-block-heading">What’s New in Firebase 9?</h3>



<h4 class="wp-block-heading">Firebase Documentation</h4>



<ul class="wp-block-list"><li><a href="https://firebase.google.com/docs/web/modular-upgrade?authuser=0" target="_blank" rel="noreferrer noopener">升級到 v9 模塊化 SDK</a></li><li><a rel="noreferrer noopener" href="https://firebase.google.com/docs/auth/web/password-auth?authuser=0" target="_blank">密碼驗證</a></li></ul>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://github.com/iamshaunjp/Getting-Started-with-Firebase-9" target="_blank">Getting-Started-with-Firebase-9 GitHub</a></li><li><a rel="noreferrer noopener" href="https://nodejs.org/en/" target="_blank">Node.js</a></li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Webpack Setup</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>建立一個專案資料夾 firebase-9-dojo</li><li>在專案裡面建立 src 資料夾</li><li>在 src 資料夾建立 index.js 檔案，當作 entry file 進入點</li><li>在專案裡面建立 dist 資料夾</li><li>在 dist 資料夾建立 index.html 檔案，並新增 index.html 內容</li><li>打開 Terminal 終端機執行初始化指令 npm init -y 快速建立 package.json 檔案</li><li>使用 Terminal 終端機安裝 webpack、webpack-cli 套件<ul><li>npm i webpack webpack-cli -D</li></ul></li><li>在專案裡新增 webpack.config.js 檔案、並設定 webpack</li><li>在 package.json “scripts” 指令設定客製化指令<ul><li>“build”: “webpack”，build 可以自行設定指令名稱</li></ul></li><li>在 index.js 新增一些程式碼，確認是否能正確運作</li><li>打開 Terminal 終端機，先使用 clear 指令清除終端機畫面、接著執行 npm run build，會產生 bundle.js 檔案</li><li>在 index.html 新增&lt;script&gt; 把 bundle.js 載入</li><li>使用 live server 插件 (沒有安裝需到 Extensions 搜尋安裝)、執行 live server</li><li>查看 Google Console 看是否有正確顯示</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  watch: true
}</code></pre>



<pre class="wp-block-code"><code>// package.json

{
  "name": "firebase-9-dojo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1",
    "build": "webpack"
  },
  "keywords": &#91;],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.2"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// index.js

console.log('hello from index.js');</code></pre>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
  &lt;script src="bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Creating a Firebase Project</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>到 Firebase Console (控制台)</li><li>Add project (新增專案)</li><li>Firebase name – Firebase 9 Dojo、然後按下繼續</li><li>我們這個專案不需要 Google Analytics (Google 分析)、然後按下建立專案、完成後按繼續</li><li>將 Firebase 新增至應用程式即可開始使用，點選網頁</li><li>Register app (註冊應用程式)<ul><li>App nickname: firebase 9 dojo website</li></ul></li><li>Continue to console (前往控制台)</li><li>點擊 1個 個應用程式、設定按鈕</li><li>在 SDK 設定和配置，選擇設定、並複製下面的程式碼</li><li>在 index.js 先把 console.log 程式碼移除、並把剛複製的程式碼貼上、在儲存之前我們先打開終端機安裝 firebase 套件，安裝後儲存<ul><li>npm install firebase</li></ul></li><li>在 index.js import 載入 firebase、複習 8 的版本、如何使用 9 的版本</li></ul>



<pre class="wp-block-code"><code>// index.js - 1 API串接資料查詢 Firebase

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};</code></pre>



<pre class="wp-block-code"><code>// index.js - 2 Firebase 8 版本

import firebase from 'firebase/app'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

firebase.initializeApp(firebaseConfig)</code></pre>



<pre class="wp-block-code"><code>// index.js - 3 Firebase 9 版本

import { initializeApp } from 'firebase/app'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

initializeApp(firebaseConfig)</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Firestore Setup &amp; Fetching Data</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>建立 Firestore Database</li><li>點擊建立資料庫、在這我們先使用以測試模式啟動</li><li>設定 Cloud Firestore 位置、然後啟用</li><li>點擊新增集合、集合 ID 叫做 books、然後按下一步</li><li>新增第一份文件、文件 ID 使用自動產生的 ID、欄位 title, 類型 string, 值 the name of the wind、欄位 author, 類型 string, 值 patrick rothfuss，然後按下儲存</li><li>再新增兩份類似的文件，欄位 title, 類型 string, 值 the wise man’s fear、欄位 author, 類型 string, 值 patrick rothfuss，然後按下儲存，欄位 title, 類型 string, 值 the final empire、欄位 author, 類型 string, 值 brandon sanders，然後按下儲存</li><li>在 index.js 新增新的註解、程式碼</li><li>使用 live server、查看 Google Console</li><li>修改 index.js get collection data 程式碼，然後再次查看 Google Console</li></ul>



<pre class="wp-block-code"><code>// index.js - API串接資料查看 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, getDocs
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// get collection data
getDocs(colRef)
  .then((snapshot) =&gt; {
    console.log(snapshot.docs)
  })</code></pre>



<pre class="wp-block-code"><code>// index.js 修改 get collection data
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, getDocs
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// get collection data
getDocs(colRef)
  .then((snapshot) =&gt; {
    let books = &#91;]
    snapshot.docs.forEach((doc) =&gt; {
      books.push({ ...doc.data(), id: doc.id })
    })
    console.log(books)
  })
  .catch(err =&gt; {
    console.log(err.message)
  })</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Adding &amp; Deleting Documents</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增樣板內容</li><li>在 index.js 新增增加文件、刪除文件程式碼</li><li>在 index.js 增加 import 內容、addDoc，然後儲存</li><li>使用 live server 開啟網頁輸入標題 the way of kings、作者 brandon anderson、然後新增書本</li><li>在 index.js 增加 import 內容、deleteDoc, doc，然後儲存</li><li>在網頁的 Google Console 找到資料 id、然後複製貼到 Document id 欄位並且按下刪除書本，會清除欄位，在網頁重新整理後會剩下3筆資料</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
  &lt;h2&gt;Firebase Firestore&lt;/h2&gt;

  &lt;form class="add"&gt;
    &lt;label for="title"&gt;Title:&lt;/label&gt;
    &lt;input type="text" name="title" required&gt;
    &lt;label for="author"&gt;Author:&lt;/label&gt;
    &lt;input type="text" name="author" required&gt;

    &lt;button&gt;add a new book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="delete"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;delete a book&lt;/button&gt;
  &lt;/form&gt;
  
  &lt;script src="bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 1 adding, deleting documents
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, getDocs
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// get collection data
getDocs(colRef)
  .then((snapshot) =&gt; {
    let books = &#91;]
    snapshot.docs.forEach((doc) =&gt; {
      books.push({ ...doc.data(), id: doc.id })
    })
    console.log(books)
  })
  .catch(err =&gt; {
    console.log(err.message)
  })
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()


})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()
  
})</code></pre>



<pre class="wp-block-code"><code>// index.js - 2 增加 import 內容、addDoc
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, getDocs,
  addDoc
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// get collection data
getDocs(colRef)
  .then((snapshot) =&gt; {
    let books = &#91;]
    snapshot.docs.forEach((doc) =&gt; {
      books.push({ ...doc.data(), id: doc.id })
    })
    console.log(books)
  })
  .catch(err =&gt; {
    console.log(err.message)
  })
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

})</code></pre>



<pre class="wp-block-code"><code>// index.js - 3 增加 import 內容、deleteDoc, doc
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, getDocs,
  addDoc, deleteDoc, doc
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// get collection data
getDocs(colRef)
  .then((snapshot) =&gt; {
    let books = &#91;]
    snapshot.docs.forEach((doc) =&gt; {
      books.push({ ...doc.data(), id: doc.id })
    })
    console.log(books)
  })
  .catch(err =&gt; {
    console.log(err.message)
  })
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})</code></pre>



<h4 class="wp-block-heading">Google Console 出現警告 – DevTools failed to load source map</h4>



<ul class="wp-block-list"><li>F12 &gt; 設定圖示 &gt; Preferences &gt; Sources 取消勾選<ul><li>Enable JavaScript source maps</li><li>Enable CSS source maps</li></ul></li></ul>



<h4 class="wp-block-heading">新增一筆資料，Google Console 沒有顯示新增的資料</h4>



<ul class="wp-block-list"><li>執行”build” 指令 npm run build</li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Real Time Collection Data</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>修改 index.js 檔案 get collection data 改成 real time collection data、import 的 getDocs 改成 onSnapshot，新增 onSnapshot 方法、刪除 getDocs 方法、然後儲存</li><li>在 Google Console 複製 “the wise man’s fear” 這筆資料 id、然後貼到欄位、按下刪除書本，可即時刪除剩下3筆資料</li><li>新增一筆資料，標題 the wise man’s fear、作者 patrick rothfuss、按下新增書本，可即時新增變成4筆資料</li></ul>



<pre class="wp-block-code"><code>// index.js - 修改 index.js 檔案
// api 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// real time collection data
onSnapshot(colRef, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})</code></pre>



<pre class="wp-block-code"><code>// Google Console
   (4) &#91;{...}, {...}, {...}, {...}]
     0: {author: 'patrick rothfuss', title: 'the name of the wind'}
     1: {author: 'brandon sanderson', title: 'the final empire'}
     2: {title: "the wise man's fear", author: 'patrick rothfuss'}
     3: {author: 'brandon sanderson', title: 'the way of kings'}
     &#91;&#91;Prototype]]: Array(0)
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Firestore Queries</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.js 增加 import 內容、新增 queries 註解</li><li>在網頁欄位標題 abc、作者 def，然後新增書本不會顯示新增資料</li><li>在網頁欄位標題 abc、作者 patrick rothfuss，會新增資料</li><li>複製標題為 abc 資料的 id 貼到欄位，然後刪除書本</li></ul>



<pre class="wp-block-code"><code>// index.js 增加 import 內容、新增 queries 註解、修改 real time collection data
// api 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, where("author", "==", "patrick rothfuss"))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Timestamps &amp; Ordering Data</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.js 新增 import 內容、在 queries 新增 orderBy，然後到 Google Console 查看會顯示錯誤</li><li>依 Google Console 顯示錯誤指示點擊連結、點擊建立索引(建立需要一些時間，完成後狀態會是已啟用)</li><li>在 Cloud Firestore 點擊資料、然後刪除集合</li><li>在 index.js adding documents 的 addDoc 新增 createdAt，在 import 新增 serverTimestamp，在 queries 修改 orderBy、移除 where，然後儲存</li><li>從網頁新增資料，標題 the wise man’s fear、作者 patrick rothfuss、然後新增書本，再新增標題 the name of the wind、作者 patrick rothfuss、然後新增書本，再新增標題 the way of kings、作者 brandon sanderson、然後新增書本</li><li>我們可以看到 Google Console 資料有依時間序列排列，在網頁新增資料，標題 the final empire、作者 brandon sanderson、然後新增書本</li><li>新增資料時可以看到 Google Console 顯示兩次資料</li></ul>



<pre class="wp-block-code"><code>// index.js - 1 新增 import 內容、在 queries 新增 orderBy
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, where("author", "==", "patrick rothfuss"), orderBy('title', 'desc'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})</code></pre>



<pre class="wp-block-code"><code>// Google Console - 顯示錯誤資訊
x  Uncaught Error in snapshot listener: FirebaseError: The query requires an index. You can create it here: url
x  GET url net::ERR_FAILED 200
&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 2 新增 createdAt、serverTimestamp，修改 orderBy，移除 where
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Fetching Single Documents</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.js import 新增 getDoc、在下面新增 get a single document 內容，然後儲存查看 Google Console</li><li>在 index.js get a single document 新增 onSnapshot 內容，然後上面的 getDoc 刪除，然後儲存</li><li>在 Cloud Firestore 更改該 id 的值為大寫 T，值 The wise man’s fear 、然後更新，查看 Google Console 可以看到標題開頭變成大寫</li></ul>



<pre class="wp-block-code"><code>// index.js - 1 import 新增 getDoc、新增 get a single document
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

getDoc(docRef)
  .then((doc) =&gt; {
    console.log(doc.data(), doc.id)
  })</code></pre>



<pre class="wp-block-code"><code>// index.js - 2 在最下面新增 onSnapshot 內容、移除上面的 getDoc
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Updating Documents</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>修改、新增 index.html 內容</li><li>在 index.js 新增 updating a document、在 import 新增 updateDoc，然後儲存</li><li>在 Google Console 複製文件的 id 到網頁上、按下更新書本</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
  &lt;h2&gt;Firebase Firestore&lt;/h2&gt;

  &lt;form class="add"&gt;
    &lt;label for="title"&gt;Title:&lt;/label&gt;
    &lt;input type="text" name="title" required&gt;
    &lt;label for="author"&gt;Author:&lt;/label&gt;
    &lt;input type="text" name="author" required&gt;

    &lt;button&gt;add a new book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="delete"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;delete a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="update"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;update a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;script src="bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 1 updating a document、在 import 新增 updateDoc
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc, updateDoc
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})

// updating a document
const updateForm = document.querySelector('.update')
updateForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', updateForm.id.value)

  updateDoc(docRef, {
    title: 'updated title'
  })
  .then(() =&gt; {
    updateForm.reset()
  })

})</code></pre>



<h3 class="wp-block-heading">Firebase Auth Setup</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>點擊左邊建立的 Authentication、開始使用</li><li>選擇電子郵件/密碼、然後啟用、儲存</li><li>在前端需要初始化驗證服務，在 index.js 新增 import、在 init services 新增內容</li></ul>



<pre class="wp-block-code"><code>// index.js - 新增 import、在 init services 新增內容
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc, updateDoc
} from 'firebase/firestore'
import {
  getAuth
} from 'firebase/auth'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()
const auth = getAuth()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})

// updating a document
const updateForm = document.querySelector('.update')
updateForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', updateForm.id.value)

  updateDoc(docRef, {
    title: 'updated title'
  })
  .then(() =&gt; {
    updateForm.reset()
  })

})</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Signing Users Up</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增 Firebase Auth 內容</li><li>在 index.js 新增 signing users up 內容，在最上面 import 新增 createUserWithEmailAndPassword，再新增 email、password 程式碼內容，然後儲存</li><li>接著到網頁輸入 email, password (隨意信箱、密碼測試)，產生錯誤，密碼需要至少6個字元 mario@netninja.dev、test12345，點擊註冊就會在 Google Console 顯示資料</li></ul>



<pre class="wp-block-code"><code>// index.html - 1 新增 Firebase Auth 內容

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
  &lt;h2&gt;Firebase Firestore&lt;/h2&gt;

  &lt;form class="add"&gt;
    &lt;label for="title"&gt;Title:&lt;/label&gt;
    &lt;input type="text" name="title" required&gt;
    &lt;label for="author"&gt;Author:&lt;/label&gt;
    &lt;input type="text" name="author" required&gt;

    &lt;button&gt;add a new book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="delete"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;delete a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="update"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;update a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;h2&gt;Firebase Auth&lt;/h2&gt;

  &lt;form class="signup"&gt;
    &lt;label for="email"&gt;email:&lt;/label&gt;
    &lt;input type="email" name="email"&gt;
    &lt;label for="password"&gt;password:&lt;/label&gt;
    &lt;input type="password" name="password"&gt;
    &lt;button&gt;signup&lt;/button&gt;
  &lt;/form&gt;

  &lt;script src="bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 1 新增 signing users up 內容
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc, updateDoc
} from 'firebase/firestore'
import {
  getAuth,
  createUserWithEmailAndPassword
} from 'firebase/auth'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()
const auth = getAuth()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})

// updating a document
const updateForm = document.querySelector('.update')
updateForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', updateForm.id.value)

  updateDoc(docRef, {
    title: 'updated title'
  })
  .then(() =&gt; {
    updateForm.reset()
  })

})

// signing users up
const signupForm = document.querySelector('.signup')
signupForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = signupForm.email.value
  const password = signupForm.password.value

  createUserWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      console.log('user created:', cred.user)
      signupForm.reset()
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Logging in &amp; Logging Out</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增 login、logout 內容</li><li>在 index.js 新增 logging in and out 內容，在最上面新增 import signOut，接著新增 logout 內容，回到網頁測試登出是否能正確執行，在最上面新增 import signInWithEmailAndPassword，接著新增 login 內容、email、password，然後儲存</li><li>到網頁使用登入功能，先使用未註冊的 email 測試，會回傳 Error (auth/user-not-found)，接著測試有註冊的 email、錯誤的密碼，會回傳 Error (auth/wrong-password)，接著使用正確已註冊的 email、密碼登入，會成功回傳訊息</li><li>點擊登出按鈕也會回傳訊息</li></ul>



<pre class="wp-block-code"><code>// index.html - 新增 login、logout 內容

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
  &lt;h2&gt;Firebase Firestore&lt;/h2&gt;

  &lt;form class="add"&gt;
    &lt;label for="title"&gt;Title:&lt;/label&gt;
    &lt;input type="text" name="title" required&gt;
    &lt;label for="author"&gt;Author:&lt;/label&gt;
    &lt;input type="text" name="author" required&gt;

    &lt;button&gt;add a new book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="delete"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;delete a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="update"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;update a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;h2&gt;Firebase Auth&lt;/h2&gt;

  &lt;form class="signup"&gt;
    &lt;label for="email"&gt;email:&lt;/label&gt;
    &lt;input type="email" name="email"&gt;
    &lt;label for="password"&gt;password:&lt;/label&gt;
    &lt;input type="password" name="password"&gt;
    &lt;button&gt;signup&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="login"&gt;
    &lt;label for="email"&gt;email:&lt;/label&gt;
    &lt;input type="email" name="email"&gt;
    &lt;label for="password"&gt;password:&lt;/label&gt;
    &lt;input type="password" name="password"&gt;
    &lt;button&gt;login&lt;/button&gt;
  &lt;/form&gt;

  &lt;button class="logout"&gt;logout&lt;/button&gt;

  &lt;script src="bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js - 新增 logging in and out
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc, updateDoc
} from 'firebase/firestore'
import {
  getAuth,
  createUserWithEmailAndPassword,
  signOut, signInWithEmailAndPassword
} from 'firebase/auth'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()
const auth = getAuth()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})

// updating a document
const updateForm = document.querySelector('.update')
updateForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', updateForm.id.value)

  updateDoc(docRef, {
    title: 'updated title'
  })
  .then(() =&gt; {
    updateForm.reset()
  })

})

// signing users up
const signupForm = document.querySelector('.signup')
signupForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = signupForm.email.value
  const password = signupForm.password.value

  createUserWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      console.log('user created:', cred.user)
      signupForm.reset()
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})

// logging in and out
const logoutButton = document.querySelector('.logout')
logoutButton.addEventListener('click', () =&gt; {
  signOut(auth)
    .then(() =&gt; {
      console.log('the user signed out')
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })
})

const loginForm = document.querySelector('.login')
loginForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = loginForm.email.value
  const password = loginForm.password.value

  signInWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      console.log('user logged in:', cred.user)
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})</code></pre>



<p>這個章節沒有提到關於使用 HTML 操作登入、登出。</p>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Listening to Auth Changes</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.js import 新增 onAuthStateChanged，在最下面新增 subscribing to auth changes 內容，把 signing users up、logging in and out 的 console 註解，然後儲存</li><li>接著到網頁重新整理，可以在 Google Console 看到 user status changed: null 的顯示</li><li>在 email 輸入 mario@netninja.dev、password 輸入 test12345，然後按下登入會回傳 user status changed: 資料內容</li><li>接著按下登出就會回傳 user status changed: null</li><li>在註冊欄位 email 輸入 luigi@netninja.dev、password 輸入 test12345，然後按下註冊，就會回傳 user status changed: 資料內容</li><li>在點擊登出，在 Google Console 會回傳訊息 user status changed: null</li></ul>



<pre class="wp-block-code"><code>// index.js - import 新增 onAuthStateChanged，新增 subscribing to auth changes 內容
// API 串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc, updateDoc
} from 'firebase/firestore'
import {
  getAuth,
  createUserWithEmailAndPassword,
  signOut, signInWithEmailAndPassword,
  onAuthStateChanged
} from 'firebase/auth'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()
const auth = getAuth()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})

// updating a document
const updateForm = document.querySelector('.update')
updateForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', updateForm.id.value)

  updateDoc(docRef, {
    title: 'updated title'
  })
  .then(() =&gt; {
    updateForm.reset()
  })

})

// signing users up
const signupForm = document.querySelector('.signup')
signupForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = signupForm.email.value
  const password = signupForm.password.value

  createUserWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      // console.log('user created:', cred.user)
      signupForm.reset()
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})

// logging in and out
const logoutButton = document.querySelector('.logout')
logoutButton.addEventListener('click', () =&gt; {
  signOut(auth)
    .then(() =&gt; {
      // console.log('the user signed out')
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })
})

const loginForm = document.querySelector('.login')
loginForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = loginForm.email.value
  const password = loginForm.password.value

  signInWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      // console.log('user logged in:', cred.user)
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})

// subscribing to auth changes
onAuthStateChanged(auth, (user) =&gt; {
  console.log('user status changed:', user)
})</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Unsubscribing from Changes</h3>



<p>可重複觀看、練習。</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 index.html 新增 unsubscribing</li><li>在 index.js 新增 unsubscribing from changes (auth &amp; db) 內容，在 real time collection data 的 onSnapshot 的地方修改新增 unsubCol 變數，在 get a single document 的 onSnapshot 的地方修改新增 unsubDoc 變數，在 subscribing to auth changes 的 onAuthStateChanged 的地方修改新增 unsubAuth 變數，接著回到 unsubscribing from changes (auth &amp; db) 繼續新增內容，然後儲存</li><li>到網頁介面，先展示上面的功能還是能使用，Firebase Firestore 的 Title abc、Author def，Firebase Auth 的 email mario@netninja.dev、password test12345、然後登入，一樣有回傳訊息</li><li>接著在Google Console按下清楚 console 內容，然後按下 unsubscribing 的按鈕，會回傳 unsubscribing 訊息，再次使用 Firebase Firestore、Firebase Auth 功能</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Firebase 9&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Getting started with firebase 9&lt;/h1&gt;
  
  &lt;h2&gt;Firebase Firestore&lt;/h2&gt;

  &lt;form class="add"&gt;
    &lt;label for="title"&gt;Title:&lt;/label&gt;
    &lt;input type="text" name="title" required&gt;
    &lt;label for="author"&gt;Author:&lt;/label&gt;
    &lt;input type="text" name="author" required&gt;

    &lt;button&gt;add a new book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="delete"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;delete a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="update"&gt;
    &lt;label for="id"&gt;Document id:&lt;/label&gt;
    &lt;input type="text" name="id" required&gt;

    &lt;button&gt;update a book&lt;/button&gt;
  &lt;/form&gt;

  &lt;h2&gt;Firebase Auth&lt;/h2&gt;

  &lt;form class="signup"&gt;
    &lt;label for="email"&gt;email:&lt;/label&gt;
    &lt;input type="email" name="email"&gt;
    &lt;label for="password"&gt;password:&lt;/label&gt;
    &lt;input type="password" name="password"&gt;
    &lt;button&gt;signup&lt;/button&gt;
  &lt;/form&gt;

  &lt;form class="login"&gt;
    &lt;label for="email"&gt;email:&lt;/label&gt;
    &lt;input type="email" name="email"&gt;
    &lt;label for="password"&gt;password:&lt;/label&gt;
    &lt;input type="password" name="password"&gt;
    &lt;button&gt;login&lt;/button&gt;
  &lt;/form&gt;

  &lt;button class="logout"&gt;logout&lt;/button&gt;

  &lt;h2&gt;Unsubscribing&lt;/h2&gt;
  &lt;button class="unsub"&gt;unsubscribe from db/auth changes&lt;/button&gt;

  &lt;script src="bundle.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.js
// API串接資料查閱 Firebase

import { initializeApp } from 'firebase/app'
import {
  getFirestore, collection, onSnapshot,
  addDoc, deleteDoc, doc,
  query, where,
  orderBy, serverTimestamp,
  getDoc, updateDoc
} from 'firebase/firestore'
import {
  getAuth,
  createUserWithEmailAndPassword,
  signOut, signInWithEmailAndPassword,
  onAuthStateChanged
} from 'firebase/auth'

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

// init firebase app
initializeApp(firebaseConfig)

// init services
const db = getFirestore()
const auth = getAuth()

// collection ref
const colRef = collection(db, 'books')

// queries
const q = query(colRef, orderBy('createdAt'))

// real time collection data
const unsubCol = onSnapshot(q, (snapshot) =&gt; {
  let books = &#91;]
  snapshot.docs.forEach((doc) =&gt; {
    books.push({ ...doc.data(), id: doc.id })
  })
  console.log(books)
})
  
// adding documents
const addBookForm = document.querySelector('.add')
addBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  addDoc(colRef, {
    title: addBookForm.title.value,
    author: addBookForm.author.value,
    createdAt: serverTimestamp()
  })
  .then(() =&gt; {
    addBookForm.reset()
  })

})

// deleting documents
const deleteBookForm = document.querySelector('.delete')
deleteBookForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', deleteBookForm.id.value)

  deleteDoc(docRef)
    .then(() =&gt; {
      deleteBookForm.reset()
    })
})

// get a single document
const docRef = doc(db, 'books', 'lS9bzDHdk7rwUNknNtnz')

const unsubDoc = onSnapshot(docRef, (doc) =&gt; {
  console.log(doc.data(), doc.id)
})

// updating a document
const updateForm = document.querySelector('.update')
updateForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const docRef = doc(db, 'books', updateForm.id.value)

  updateDoc(docRef, {
    title: 'updated title'
  })
  .then(() =&gt; {
    updateForm.reset()
  })

})

// signing users up
const signupForm = document.querySelector('.signup')
signupForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = signupForm.email.value
  const password = signupForm.password.value

  createUserWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      // console.log('user created:', cred.user)
      signupForm.reset()
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})

// logging in and out
const logoutButton = document.querySelector('.logout')
logoutButton.addEventListener('click', () =&gt; {
  signOut(auth)
    .then(() =&gt; {
      // console.log('the user signed out')
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })
})

const loginForm = document.querySelector('.login')
loginForm.addEventListener('submit', (e) =&gt; {
  e.preventDefault()

  const email = loginForm.email.value
  const password = loginForm.password.value

  signInWithEmailAndPassword(auth, email, password)
    .then((cred) =&gt; {
      // console.log('user logged in:', cred.user)
    })
    .catch((err) =&gt; {
      console.log(err.message)
    })

})

// subscribing to auth changes
const unsubAuth = onAuthStateChanged(auth, (user) =&gt; {
  console.log('user status changed:', user)
})

// unsubscribing from changes (auth &amp; db)
const unsubButton = document.querySelector('.unsub')
unsubButton.addEventListener('click', () =&gt; {
  console.log('unsubscribing')
  unsubCol()
  unsubDoc()
  unsubAuth()
})</code></pre>



<p>最後，關於 Firebase 更多學習應用，可以參考瀏覽說明文件 Firebase Documentation 來使用。</p>



<h2 class="wp-block-heading">Next Steps</h2>



<h3 class="wp-block-heading">Bonus Lecture: Next Steps</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://www.youtube.com/c/TheNetNinja/playlists" target="_blank">The Net Ninja Youtube Playlists 連結</a></li><li><a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=aN1LnNq4z54&amp;list=PL4cUxeGkcC9jUPIes_B8vRjn1_GaplOPQ" target="_blank">Firebase Auth Tutorial</a></li><li><a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=r6I-Ahc0HB4&amp;list=PL4cUxeGkcC9g6m_6Sld9Q4jzqdqHd2HiD" target="_blank">Regular Expressions (RegEx) Tutorial</a></li><li><a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=w-7RQ46RgxU&amp;list=PL4cUxeGkcC9gcy9lrvMJ75z9maRw4byYp" target="_blank">Node JS Tutorial for Beginners</a>&nbsp;(舊版)</li><li><a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=OxIDLw0M-m0&amp;list=PL4cUxeGkcC9ij8CfkAY2RAGb-tmkNwQHG" target="_blank">Complete React Tutorial (with Redux)</a></li><li><a href="https://www.udemy.com/user/47fd83f6-5e4a-4e87-a0f0-519ac51f91b6/" target="_blank" rel="noreferrer noopener">Udemy Course – The Net Ninja(Shaun Pelling)</a></li><li><a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=zb3Qk8SG5Ms&amp;list=PL4cUxeGkcC9jsz4LDYc6kv3ymONOKxwBU" target="_blank">Node.js Crash Course Tutorial</a>&nbsp;(新版)</li></ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Modern JavaScript (3)</title>
		<link>/wordpress_blog/modern-javascript-3/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Fri, 11 Feb 2022 04:18:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=508</guid>

					<description><![CDATA[(Complete guide, from Novice to  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>(Complete guide, from Novice to Ninja)</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Learning Udemy Course：<a href="https://www.udemy.com/course/modern-javascript-from-novice-to-ninja/" target="_blank" rel="noreferrer noopener">Modern JavaScript</a></p><cite>建立者：The Net Ninja (Shaun Pelling)</cite></blockquote>



<p>Learn Modern JavaScript from the very start to ninja-level &amp; build awesome JavaScript applications.</p>



<h4 class="wp-block-heading">您會學到</h4>



<ul class="wp-block-list"><li>Learn how to program with modern JavaScript, from the very beginning to more advanced topics</li><li>Learn all about OOP (object-oriented programming) with JavaScript, working with prototypes &amp; classes</li><li>Learn how to create real-world front-end applications with JavaScript (quizes, weather apps, chat rooms etc)</li><li>Learn how to make useful JavaScript driven UI components like popups, drop-downs, tabs, tool-tips &amp; more.</li><li>Learn how to use modern, cutting-edge JavaScript features today by using a modern workflow (Babel &amp; Webpack)</li><li>Learn how to use real-time databases to store, retrieve and update application data</li><li>Explore API’s to make the most of third-party data (such as weather information)</li></ul>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Project – Weather App</h2>



<h3 class="wp-block-heading">Project Preview &amp; Setup</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>創建一個專案資料夾 weather_app</li><li>新增一個 index.html 檔案</li><li>index.html 使用 doc + tab 建立環境、載入 CSS、Bootstrap CDN</li><li>新增一個 scripts 資料夾、並建立 app.js、forecast.js 檔案</li><li>在 index.html 載入 JS</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Weather&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  

  &lt;script src="scripts/forecast.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">HTML &amp; CSS Template</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://github.com/iamshaunjp/modern-javascript/tree/lesson-100" target="_blank" rel="noreferrer noopener">GitHub files for this lesson (HTML template)</a></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Weather&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;div class="container my-5 mx-auto"&gt;

    &lt;h1 class="text-muted text-center my-4"&gt;Ninja Weather&lt;/h1&gt;

    &lt;form class="change-location my-4 text-center text-muted"&gt;
      &lt;label for="city"&gt;Enter a location for weather information&lt;/label&gt;
      &lt;input type="text" name="city" class="form-control p-4"&gt;
    &lt;/form&gt;

    &lt;div class="card shadow-lg rounded"&gt;
      &lt;img src="https://via.placeholder.com/400x300" class="time card-img-top" alt=""&gt;
      &lt;div class="icon bg-light mx-auto text-center"&gt;
        &lt;!-- icon --&gt;
      &lt;/div&gt;
      &lt;div class="text-muted text-uppercase text-center details"&gt;
        &lt;h5 class="my-3"&gt;City name&lt;/h5&gt;
        &lt;div class="my-3"&gt;Weather condition&lt;/div&gt;
        &lt;div class="display-4 my-4"&gt;
          &lt;span&gt;temp&lt;/span&gt;
          &lt;span&gt;&amp;deg;C&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

  &lt;/div&gt;
  

  &lt;script src="scripts/forecast.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

body{
  background: #eeedec;
  letter-spacing: 0.2em;
  font-size: 0.8em;
}
.container{
  max-width: 400px;
}</code></pre>



<h4 class="wp-block-heading">Placeholder Image</h4>



<ul class="wp-block-list"><li><a href="https://placeholder.com/" target="_blank" rel="noreferrer noopener">網站連結</a></li></ul>



<h3 class="wp-block-heading">AccuWeather API</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://developer.accuweather.com/" target="_blank" rel="noreferrer noopener">AccuWeather API Website</a></li></ul>



<h4 class="wp-block-heading">API Reference</h4>



<ul class="wp-block-list"><li>Locations API<ul><li>Text Search – City Search</li></ul></li><li>Current Conditions API<ul><li>Current Conditions – Current Conditions</li></ul></li></ul>



<h4 class="wp-block-heading">個人練習</h4>



<ul class="wp-block-list"><li>政府資料凱放平臺</li><li>中央氣象局開放資料平臺之資料擷取 API</li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Get City API Call</h3>



<pre class="wp-block-code"><code>// forecast.js

const key = 'rdec-key-123-45678-011121314';

const getCity = async (city) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&amp;locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location&#91;0]);
  
return data.records.location&#91;0];

};

getCity('臺南市')
  .then(data =&gt; console.log(data))
  .catch(err =&gt; console.log(err));</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Get Weather API Call</h3>



<pre class="wp-block-code"><code>// forecast.js - 1

const key = 'rdec-key-123-45678-011121314';

// get weather information
const getWeather = async (id = 'F-C0032-001') =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  return data.records.location;

};

// get city information
const getCity = async (city) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&amp;locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location&#91;0]);
  
return data.records.location;

};

getCity('臺南市').then(data =&gt; {
    return getWeather(data.key);
  }).then(data =&gt; {
    console.log(data);
  }).catch(err =&gt; console.log(err));
</code></pre>



<pre class="wp-block-code"><code>// forecast.js - 2

const key = 'rdec-key-123-45678-011121314';

// get weather information
const getWeather = async (id) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  return data;

};

// get city information
const getCity = async (city) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&amp;locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location&#91;0]);
  
return data;

};

getCity('臺南市').then(data =&gt; {
    return getWeather(data.result.resource_id);
  }).then(data =&gt; {
    console.log(data);
  }).catch(err =&gt; console.log(err));
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Updating the Location</h3>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(cityDets.result.resource_id);

  return {
    cityDets: cityDets,
    weather: weather
  };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; console.log(data))
  .catch(err =&gt; console.log(err));

});</code></pre>



<h3 class="wp-block-heading">Object Shorthand Notation</h3>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(cityDets.result.resource_id);

  return { cityDets, weather };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; console.log(data))
  .catch(err =&gt; console.log(err));

});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Updating the UI</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Weather&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;div class="container my-5 mx-auto"&gt;

    &lt;h1 class="text-muted text-center my-4"&gt;Ninja Weather&lt;/h1&gt;

    &lt;form class="change-location my-4 text-center text-muted"&gt;
      &lt;label for="city"&gt;Enter a location for weather information&lt;/label&gt;
      &lt;input type="text" name="city" class="form-control p-4"&gt;
    &lt;/form&gt;

    &lt;div class="card shadow-lg rounded d-none"&gt;
      &lt;img src="https://via.placeholder.com/400x300" class="time card-img-top" alt=""&gt;
      &lt;div class="icon bg-light mx-auto text-center"&gt;
        &lt;!-- icon --&gt;
      &lt;/div&gt;
      &lt;div class="text-muted text-uppercase text-center details"&gt;
        &lt;h5 class="my-3"&gt;City name&lt;/h5&gt;
        &lt;div class="my-3"&gt;Weather condition&lt;/div&gt;
        &lt;div class="display-4 my-4"&gt;
          &lt;span&gt;temp&lt;/span&gt;
          &lt;span&gt;&amp;deg;C&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

  &lt;/div&gt;
  

  &lt;script src="scripts/forecast.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');

const updateUI = (data) =&gt; {
  
  const cityDets = data.cityDets;
  const weather = data.weather;

  // update details template
  details.innerHTML = `
    &lt;h4 class="my-3"&gt;今明36小時天氣預報&lt;/h4&gt;
    &lt;h5 class="my-3"&gt;${cityDets.records.location&#91;0].locationName}&lt;/h5&gt;
    &lt;div class="my-3"&gt;${cityDets.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterName}&lt;/div&gt;
    &lt;div&gt;↓&lt;/div&gt;
    &lt;div class="my-3"&gt;&lt;br&gt;${cityDets.records.location&#91;0].weatherElement&#91;0].time&#91;1].parameter.parameterName}&lt;/div&gt;
    &lt;div&gt;↓&lt;/div&gt;
    &lt;div class="my-3"&gt;&lt;br&gt;${cityDets.records.location&#91;0].weatherElement&#91;0].time&#91;2].parameter.parameterName}&lt;/div&gt;
    &lt;div class="display-4 my-4"&gt;
      &lt;span&gt;temp&lt;/span&gt;&lt;br&gt;
      &lt;span&gt;${cityDets.records.location&#91;0].weatherElement&#91;2].time&#91;0].parameter.parameterName} - ${cityDets.records.location&#91;0].weatherElement&#91;4].time&#91;0].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
      &lt;div&gt;↓&lt;/div&gt;
      &lt;span&gt;${cityDets.records.location&#91;0].weatherElement&#91;2].time&#91;1].parameter.parameterName} - ${cityDets.records.location&#91;0].weatherElement&#91;4].time&#91;1].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
      &lt;div&gt;↓&lt;/div&gt;
      &lt;span&gt;${cityDets.records.location&#91;0].weatherElement&#91;2].time&#91;2].parameter.parameterName} - ${cityDets.records.location&#91;0].weatherElement&#91;4].time&#91;2].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
    &lt;/div&gt;
  `;

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(cityDets.result.resource_id);

  console.log(cityDets);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; updateUI(data))
  .catch(err =&gt; console.log(err));

});</code></pre>



<pre class="wp-block-code"><code>// forecast.js

const key = 'rdec-key-123-45678-011121314';

// get weather information
const getWeather = async (id) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  
  return data;

};

// get city information
const getCity = async (city) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&amp;locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location&#91;0]);
  
  return data;

};
</code></pre>



<h3 class="wp-block-heading">Destructuring (解構賦值)</h3>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');

const updateUI = (data) =&gt; {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    &lt;h4 class="my-3"&gt;今明36小時天氣預報&lt;/h4&gt;
    &lt;h5 class="my-3"&gt;${cityDets.records.location&#91;0].locationName}&lt;/h5&gt;
    &lt;div class="my-3"&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterName}&lt;/div&gt;
    &lt;div&gt;↓&lt;/div&gt;
    &lt;div class="my-3"&gt;&lt;br&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;1].parameter.parameterName}&lt;/div&gt;
    &lt;div&gt;↓&lt;/div&gt;
    &lt;div class="my-3"&gt;&lt;br&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;2].parameter.parameterName}&lt;/div&gt;
    &lt;div class="display-4 my-4"&gt;
      &lt;span&gt;temp&lt;/span&gt;&lt;br&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;0].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;0].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
      &lt;div&gt;↓&lt;/div&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;1].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;1].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
      &lt;div&gt;↓&lt;/div&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;2].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;2].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
    &lt;/div&gt;
  `;

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; updateUI(data))
  .catch(err =&gt; console.log(err));

});</code></pre>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Weather&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;div class="container my-5 mx-auto"&gt;

    &lt;h1 class="text-muted text-center my-4"&gt;Ninja Weather&lt;/h1&gt;

    &lt;form class="change-location my-4 text-center text-muted"&gt;
      &lt;label for="city"&gt;Enter a location for weather information&lt;/label&gt;
      &lt;input type="text" name="city" class="form-control p-4"&gt;
    &lt;/form&gt;

    &lt;div class="card shadow-lg rounded d-none"&gt;
      &lt;img src="https://via.placeholder.com/400x300" class="time card-img-top" alt=""&gt;
      &lt;div class="icon bg-light mx-auto text-center"&gt;
        &lt;!-- icon --&gt;
      &lt;/div&gt;
      &lt;div class="text-muted text-uppercase text-center details"&gt;
        &lt;h5 class="my-3"&gt;City name&lt;/h5&gt;
        &lt;div class="my-3"&gt;Weather condition&lt;/div&gt;
        &lt;div class="display-4 my-4"&gt;
          &lt;span&gt;temp&lt;/span&gt;
          &lt;span&gt;&amp;deg;C&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

  &lt;/div&gt;
  

  &lt;script src="scripts/forecast.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// forecast.js

const key = 'rdec-key-123-45678-011121314';
const id = 'F-C0032-001';

// get weather information
const getWeather = async (city) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
  const query = `${id}?Authorization=${key}&amp;locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data.records.location);
  
  return data;

};

// get city information
const getCity = async (city) =&gt; {

  const base = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
  const query = `?Authorization=${key}&amp;locationName=${city}`;

  const response = await fetch(base + query);
  const data = await response.json();

  // console.log(data);
  // console.log(data.records.location&#91;0]);
  
  return data;

};
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Weather Icons &amp; images</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Weather&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;div class="container my-5 mx-auto"&gt;

    &lt;h1 class="text-muted text-center my-4"&gt;臺灣天氣&lt;/h1&gt;

    &lt;form class="change-location my-4 text-center text-muted"&gt;
      &lt;label for="city"&gt;輸入地點取得天氣資訊&lt;/label&gt;&lt;br&gt;
      &lt;span class="text-muted"&gt;需輸入完整縣市名稱(例如:臺北市)&lt;/span&gt;&lt;br&gt;&lt;br&gt;
      &lt;input type="text" name="city" class="form-control p-4"&gt;
    &lt;/form&gt;

    &lt;div class="card shadow-lg rounded d-none"&gt;
      &lt;img src="https://via.placeholder.com/400x300" class="time card-img-top" alt=""&gt;
      &lt;div class="icon bg-light mx-auto text-center"&gt;
        &lt;!-- icon --&gt;
        &lt;img src="" alt=""&gt;
      &lt;/div&gt;
      &lt;div class="text-muted text-uppercase text-center details"&gt;
        &lt;h5 class="my-3"&gt;City name&lt;/h5&gt;
        &lt;div class="my-3"&gt;Weather condition&lt;/div&gt;
        &lt;div class="display-4 my-4"&gt;
          &lt;span&gt;temp&lt;/span&gt;
          &lt;span&gt;&amp;deg;C&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

  &lt;/div&gt;
  

  &lt;script src="scripts/forecast.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

const updateUI = (data) =&gt; {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    &lt;h4 class="my-3"&gt;12小時內天氣預報&lt;/h4&gt;
    &lt;h5 class="my-3"&gt;${cityDets.records.location&#91;0].locationName}&lt;/h5&gt;
    &lt;div class="my-3"&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterName}&lt;/div&gt;
    &lt;div class="display-4 my-4"&gt;
      &lt;span&gt;氣溫&lt;/span&gt;&lt;br&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;0].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;0].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
    &lt;/div&gt;
  `;

  // update the night/day &amp; icon images
  const iconSrc = `img/icons/${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);

  let timeSrc = null;
  if(weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("06:00:00") &gt;= 0){
    timeSrc = 'img/day.svg';
  } else {
    timeSrc = 'img/night.svg';
  }
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  // console.log(weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; updateUI(data))
  .catch(err =&gt; console.log(err));

});</code></pre>



<pre class="wp-block-code"><code>// style.css

body{
  background: #eeedec;
  letter-spacing: 0.2em;
  font-size: 0.8em;
}
.container{
  max-width: 400px;
}
.icon{
  position: relative;
  top: -50px;
  border-radius: 50%;
  width: 100px;
  margin-bottom: -50px;
}</code></pre>



<h3 class="wp-block-heading">Ternary Operator (三元運算子)</h3>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

const updateUI = (data) =&gt; {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    &lt;h4 class="my-3"&gt;12小時內天氣預報&lt;/h4&gt;
    &lt;h5 class="my-3"&gt;${cityDets.records.location&#91;0].locationName}&lt;/h5&gt;
    &lt;div class="my-3"&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterName}&lt;/div&gt;
    &lt;div class="display-4 my-4"&gt;
      &lt;span&gt;氣溫&lt;/span&gt;&lt;br&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;0].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;0].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
    &lt;/div&gt;
  `;

  // update the night/day &amp; icon images
  const iconSrc = `img/icons/${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);


  let timeSrc = weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("06:00:00") &gt;= 0 ? 'img/day.svg' : 'img/night.svg';

  // let timeSrc = null;
  // if(weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("06:00:00") &gt;= 0){
  //   timeSrc = 'img/day.svg';
  // } else {
  //   timeSrc = 'img/night.svg';
  // }
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  // console.log(weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; updateUI(data))
  .catch(err =&gt; console.log(err));

});

// ternary operator
// const result = true ? 'value 1' : 'value 2';
// console.log(result);</code></pre>



<h2 class="wp-block-heading">Local Storage</h2>



<h3 class="wp-block-heading">What is Local Storage?</h3>



<h4 class="wp-block-heading">Application Data</h4>



<ul class="wp-block-list"><li>Set up a database to store &amp; retrieve data</li><li>Use local storage to store and retrieve data</li></ul>



<h4 class="wp-block-heading">Local Storage</h4>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-4 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1435" height="705" data-id="510" src="/wordpress_blog/wp-content/uploads/2022/04/localstorage-1.png" alt="" class="wp-image-510"/><figcaption>Local Storage &#8211; 1</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1461" height="613" data-id="511" src="/wordpress_blog/wp-content/uploads/2022/04/localstorage-2.png" alt="" class="wp-image-511"/><figcaption>Local Storage &#8211; 2</figcaption></figure>
</figure>



<pre class="wp-block-code"><code>// Google Console
&gt;  window
&lt;  Window&nbsp;{window: Window, self: Window, document: document, name: '', location: Location,&nbsp;…}
&gt;</code></pre>



<h3 class="wp-block-heading">Storing &amp; Getting Data</h3>



<ul class="wp-block-list"><li>Google Console</li><li>sandbox.js</li><li>Google Application → Storage → Local Storage</li></ul>



<pre class="wp-block-code"><code>// Google Console - 1
&gt;  window.localStorage
&lt;  Storage {length: 0}
&gt;  localStorage
&lt;  Storage {length: 0}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

// store data in local storage
localStorage.setItem('name', 'mario');
localStorage.setItem('age', 50);


// get data from local storage
let name = localStorage.getItem('name');
let age = localStorage.getItem('age');

console.log(name, age);

// updating data
localStorage.setItem('name', 'luigi');
localStorage.age = '40';

name = localStorage.getItem('name');
age = localStorage.getItem('age');
console.log(name, age);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2</code></pre>



<h3 class="wp-block-heading">Deleting Storage Data</h3>



<ul class="wp-block-list"><li>sandbox.js</li><li>Google Console</li><li>Google Application</li></ul>



<pre class="wp-block-code"><code>// sandbox.js - 1

// store data in local storage
localStorage.setItem('name', 'mario');
localStorage.setItem('age', 50);

// get data from local storage
let name = localStorage.getItem('name');
let age = localStorage.getItem('age');

console.log(name, age);

// deleting data from local storage

</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   mario 50
&gt;  localStorage
&lt;  Storage {name: 'mario', age: '50', length: 2}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

// store data in local storage
localStorage.setItem('name', 'mario');
localStorage.setItem('age', 50);

// get data from local storage
let name = localStorage.getItem('name');
let age = localStorage.getItem('age');

console.log(name, age);

// deleting data from local storage
// localStorage.removeItem('name');
localStorage.clear();

name = localStorage.getItem('name');
age = localStorage.getItem('age');

console.log(name, age);
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   mario 50
   null null
&gt;</code></pre>



<h3 class="wp-block-heading">Stringifying &amp; Parsing Data</h3>



<ul class="wp-block-list"><li>sandbox.js</li><li>Google Console</li><li>Google Application</li></ul>



<pre class="wp-block-code"><code>// sandbox.js

const todos = &#91;
  {text: 'play mariokart', author: 'shaun'},
  {text: 'buy some milk', author: 'mario'},
  {text: 'buy some bread', author: 'luigi'}
];

// console.log(JSON.stringify(todos));
localStorage.setItem('todos', JSON.stringify(todos));

const stored = localStorage.getItem('todos');

console.log(JSON.parse(stored));</code></pre>



<pre class="wp-block-code"><code>// Google Console
   (3) &#91;{...}, {...}, {...}]
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Updating the Weather App</h3>



<ul class="wp-block-list"><li>app.js</li><li>Google Application</li></ul>



<pre class="wp-block-code"><code>// app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');

const updateUI = (data) =&gt; {
  
  // console.log(data);
  // const cityDets = data.cityDets;
  // const weather = data.weather;

  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    &lt;h4 class="my-3"&gt;12小時內天氣預報&lt;/h4&gt;
    &lt;h5 class="my-3"&gt;${cityDets.records.location&#91;0].locationName}&lt;/h5&gt;
    &lt;div class="my-3"&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterName}&lt;/div&gt;
    &lt;div class="display-4 my-4"&gt;
      &lt;span&gt;氣溫&lt;/span&gt;&lt;br&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;0].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;0].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
    &lt;/div&gt;
  `;

  // update the night/day &amp; icon images
  const iconSrc = `img/icons/${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);


  let timeSrc = weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("06:00:00") || weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("12:00:00") &gt;= 0 ? 'img/day.svg' : 'img/night.svg';

  // let timeSrc = null;
  // if(weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("06:00:00") &gt;= 0){
  //   timeSrc = 'img/day.svg';
  // } else {
  //   timeSrc = 'img/night.svg';
  // }
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

const updateCity = async (city) =&gt; {

  // console.log(city);
  const cityDets = await getCity(city);
  const weather = await getWeather(city);

  console.log(cityDets);
  console.log(weather);
  // console.log(weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime);
  return { cityDets, weather };

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  updateCity(city)
  .then(data =&gt; updateUI(data))
  .catch(err =&gt; console.log(err));

  // set local storage
  localStorage.setItem('city', city);

});

// ternary operator
// const result = true ? 'value 1' : 'value 2';
// console.log(result);

if(localStorage.getItem('city')){
  updateCity(localStorage.getItem('city'))
    .then(data =&gt; updateUI(data))
    .catch(err =&gt; console.log(err));
};</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Object Oriented JavaScript (物件導向)</h2>



<h3 class="wp-block-heading">What is OOP? (物件導向程式設計)</h3>



<pre class="wp-block-code"><code>// Google Console
&gt;  const names = &#91;'shaun', 'crystal']
&lt;  undefined
&gt;  names
&lt;  (2) &#91;"shaun", "crystal"]
&gt;  const ages = new Array(20,25,30)
&lt;  undefined
&gt;  ages
&lt;  (3) &#91;20, 25, 30]
&gt;  const userOne = {}
&lt;  undefined
&gt;  userOne
&lt;  {}
&gt;  const userTwo = new Object();
&lt;  undefined
&gt;  userTwo
&lt;  {}
&gt;  const name = 'mario'
&lt;  undefined
&gt;  name
&lt;  "mario"
&gt;  name.length
&lt;  5
&gt;  name.toUpperCase()
&lt;  "MARIO"
&gt;  const nameTwo = new String('ryu');
&lt;  undefined
&gt;  nameTwo
&lt;  String {"ryu"}
&gt;  new Number(5)
&lt;  Number {5}
&gt;  new Boolean(true)
&lt;  Boolean {true}
&gt;</code></pre>



<h3 class="wp-block-heading">Object Literal Recap (物件實字回顧)</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Object Oriented JavaScript&lt;/h1&gt;
    
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

const userOne = {
  username: 'ryu',
  email: 'ryu@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userOne.email, userOne.username);
userOne.login();

const userTwo = {
  username: 'chun li',
  email: 'chun.li@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userTwo.email, userTwo.username);
userTwo.login();

</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   ryu@thenetninja.co.uk ryu
   the user logged in
   chun.li@thenetninja.co.uk chun li
   the user logged in
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

const userOne = {
  username: 'ryu',
  email: 'ryu@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userOne.email, userOne.username);
userOne.login();

const userTwo = {
  username: 'chun li',
  email: 'chun.li@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userTwo.email, userTwo.username);
userTwo.login();

const userThree = new User('shaun@thenetninja.co.uk', 'shaun');</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Classes (類別)</h3>



<pre class="wp-block-code"><code>// sandbox.js

const userOne = {
  username: 'ryu',
  email: 'ryu@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userOne.email, userOne.username);
userOne.login();

const userTwo = {
  username: 'chun li',
  email: 'chun.li@thenetninja.co.uk',
  login(){
    console.log('the user logged in');
  },
  logout(){
    console.log('the user logged out');
  }
};

console.log(userTwo.email, userTwo.username);
userTwo.login();

const userThree = new User('shaun@thenetninja.co.uk', 'shaun');</code></pre>



<h4 class="wp-block-heading">Classes</h4>



<ul class="wp-block-list"><li>A Class is like a blueprint(藍圖) for an object (it describes how one should be made)</li></ul>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-5 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1536" height="553" data-id="513" src="/wordpress_blog/wp-content/uploads/2022/04/Classes-1.png" alt="" class="wp-image-513"/><figcaption>Classes &#8211; 1</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1211" height="599" data-id="514" src="/wordpress_blog/wp-content/uploads/2022/04/Classes-2.png" alt="" class="wp-image-514"/><figcaption>Classes &#8211; 2</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1536" height="603" data-id="515" src="/wordpress_blog/wp-content/uploads/2022/04/Classes-3.png" alt="" class="wp-image-515"/><figcaption>Classes &#8211; 3</figcaption></figure>
</figure>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Class Constructors (類別建構子)</h3>



<pre class="wp-block-code"><code>// sandbox.js - 1

class User {
  constructor(){
    // set up properties
    this.username = 'mario';
  }
}

const userOne = new User();
const userTwo = new User();

console.log(userOne, userTwo);

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   User {username: 'mario'} User {username: 'mario'}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

class User {
  constructor(username){
    // set up properties
    this.username = username;
  }
}

const userOne = new User('mario');
const userTwo = new User('luigi');

console.log(userOne, userTwo);

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   User {username: 'mario'} User {username: 'luigi'}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Class Methods &amp; Method Chaining</h3>



<pre class="wp-block-code"><code>// sandbox.js - 1

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
  }
  login(){
    console.log(`${this.username} just logged in`);
  }
  logout(){
    console.log(`${this.username} just logged out`);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
userTwo.login();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario just logged in
   luigi just logged in
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox - 2

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
  }
  login(){
    console.log(`${this.username} just logged in`);
  }
  logout(){
    console.log(`${this.username} just logged out`);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.logout();
userTwo.logout();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario just logged out
   luigi just logged out
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);

userOne.login().incScore().incScore().logout();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   mario just logged in
   mario has a score of 1
   mario has a score of 2
   mario just logged out
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Class Inheritance (類別繼承) (subclasses) (子類別)</h3>



<pre class="wp-block-code"><code>// sandbox.js - 1

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{

}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);

</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
&gt;</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
&gt;  userThree.login();
   shaun just logged in
&lt;  Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox - 3

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{
  deleteUser(user){
    users = users.filter((u) =&gt; {
      return u.username !== user.username;
    });
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);

let users = &#91;userOne, userTwo, userThree];
console.log(users);

userThree.deleteUser(userTwo);
console.log(users);
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
   (3) &#91;User, User, Admin]
   (2) &#91;User, Admin]
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3 縮寫

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{
  deleteUser(user){
    users = users.filter(u =&gt; u.username !== user.username);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);

let users = &#91;userOne, userTwo, userThree];
console.log(users);

userThree.deleteUser(userTwo);
console.log(users);
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3 縮寫
   User {username: 'mario', email: 'mario@thenetninja.co.uk', score: 0}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk', score: 0}
   Admin {username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0}
   (3) &#91;User, User, Admin]
   (2) &#91;User, Admin]
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Super()</h3>



<pre class="wp-block-code"><code>// sandbox.js

class User {
  constructor(username, email){
    // set up properties
    this.username = username;
    this.email = email;
    this.score = 0;
  }
  login(){
    console.log(`${this.username} just logged in`);
    return this;
  }
  logout(){
    console.log(`${this.username} just logged out`);
    return this;
  }
  incScore(){
    this.score += 1;
    console.log(`${this.username} has a score of ${this.score}`);
    return this;
  }
}

class Admin extends User{
  constructor(username, email, title){
    super(username, email);
    this.title = title;
  }
  deleteUser(user){
    users = users.filter(u =&gt; u.username !== user.username);
  }
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree = new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userThree);
</code></pre>



<pre class="wp-block-code"><code>// Google Console
   Admin&nbsp;{username: 'shaun', email: 'shaun@thenetninja.co.uk', score: 0, title: 'black-belt-ninja'}
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Constructors (under the hood)</h3>



<pre class="wp-block-code"><code>// sandbox.js

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
  this.login = function(){
    console.log(`${this.username} has logged in`);
  };
}

// class User {
//   constructor(username, email){
//     // set up properties
//     this.username = username;
//     this.email = email;
//   }
// }

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();

// the 'new' keyword
// 1 - it creates a new empty object {}
// 2 - it binds the value of 'this' to the new empty object
// 3 - it calls the constructor function to 'build' the object</code></pre>



<pre class="wp-block-code"><code>// Google Console
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk', login: ƒ}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk', login: ƒ}
   mario has logged in
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Prototype Model (原型模型)</h3>



<p>不懂可以重複觀看、練習。</p>



<pre class="wp-block-code"><code>// sandbox.js - 1

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
  this.login = function(){
    console.log(`${this.username} has logged in`);
  };
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   User User
   mario has logged in
&gt;  const nums = &#91;1,2,3,4,5]
&lt;  undefined
&gt;  nums
&lt;  (5) &#91;1,2,3,4,5]
&gt;</code></pre>



<h4 class="wp-block-heading">Prototypes (原型)</h4>



<ul class="wp-block-list"><li>Every object in JavaScript has a prototype</li><li>Prototypes contain all the methods for that object type</li></ul>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-6 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1476" height="516" data-id="518" src="/wordpress_blog/wp-content/uploads/2022/04/prototype-1.png" alt="" class="wp-image-518"/><figcaption>Prototype &#8211; 1</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1536" height="674" data-id="517" src="/wordpress_blog/wp-content/uploads/2022/04/prototype-2.png" alt="" class="wp-image-517"/><figcaption>Prototype &#8211; 2</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1536" height="681" data-id="519" src="/wordpress_blog/wp-content/uploads/2022/04/prototype-3.png" alt="" class="wp-image-519"/><figcaption>Prototype &#8211; 3</figcaption></figure>
</figure>



<pre class="wp-block-code"><code>// Google Console - 2
   User User
   mario has logged in
&gt;  const nums = &#91;1,2,3,4,5]
&lt;  undefined
&gt;  nums
&lt;  (5) &#91;1,2,3,4,5]
&gt;  Array.prototype
&lt;  &#91;constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ,&nbsp;…]
&gt;  Date.prototype
&lt;  {constructor: ƒ, toString: ƒ, toDateString: ƒ, toTimeString: ƒ, toISOString: ƒ,&nbsp;…}
&gt;  User.prototype
&lt;  {constructor: ƒ}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario has logged in
&gt;  User.prototype
&lt;  {login: f, constructor: f}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 4

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login();
userOne.logout();
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 4
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario has logged in
   mario has logged out
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 5
// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');

console.log(userOne, userTwo);
userOne.login().logout();
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 5
   User {username: 'mario', email: 'mario@thenetninja.co.uk'}
   User {username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   mario has logged in
   mario has logged out
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Prototypal Inheritance</h3>



<p>不懂可以重複觀看、練習。</p>



<pre class="wp-block-code"><code>// sandbox.js - 1

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email){
  User.call(this, username, email);
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk');

console.log(userOne, userTwo, userThree);
userOne.login().logout();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin&nbsp;{username: 'shaun', email: 'shaun@thenetninja.co.uk'}
   mario has logged in
   mario has logged out
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email, title){
  User.call(this, username, email);
  this.title = title;
}

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userOne, userTwo, userThree);
userOne.login().logout();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin&nbsp;{username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
&gt;</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin&nbsp;{username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
&gt;  Admin.prototype
&lt;  {constructor: f}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 4

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email, title){
  User.call(this, username, email);
  this.title = title;
}

Admin.prototype = Object.create(User.prototype);

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userOne, userTwo, userThree);
userOne.login().logout();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 4
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin&nbsp;{username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
&gt;  Admin.prototype
&lt;  User {}
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 5

// constructor functions

function User(username, email){
  this.username = username;
  this.email = email;
}

User.prototype.login = function(){
  console.log(`${this.username} has logged in`);
  return this;
};

User.prototype.logout = function(){
  console.log(`${this.username} has logged out`);
  return this;
};

function Admin(username, email, title){
  User.call(this, username, email);
  this.title = title;
}

Admin.prototype = Object.create(User.prototype);

Admin.prototype.deleteUser = function(){
  // delete a user
};

const userOne = new User('mario', 'mario@thenetninja.co.uk');
const userTwo = new User('luigi', 'luigi@thenetninja.co.uk');
const userThree =  new Admin('shaun', 'shaun@thenetninja.co.uk', 'black-belt-ninja');

console.log(userOne, userTwo, userThree);
userOne.login().logout();</code></pre>



<pre class="wp-block-code"><code>// Google Console - 5
   User&nbsp;{username: 'mario', email: 'mario@thenetninja.co.uk'}
   User&nbsp;{username: 'luigi', email: 'luigi@thenetninja.co.uk'}
   Admin&nbsp;{username: 'shaun', email: 'shaun@thenetninja.co.uk', title: 'black-belt-ninja'}
   mario has logged in
   mario has logged out
&gt;</code></pre>



<h3 class="wp-block-heading">Built-in Objects</h3>



<pre class="wp-block-code"><code>// Google Console
&gt;  new Array(1,2,3,4,5)
&lt;  (5) &#91;1,2,3,4,5]
&gt;  new Object
&lt;  {}
&gt;  new XMLHttpRequest
&lt;  XMLHttpRequest&nbsp;{onreadystatechange: null, readyState: 0, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload,&nbsp;…}
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Making a Forecast Class (weather app)</h3>



<pre class="wp-block-code"><code>// scripts/forecast.js

class Forecast{
  constructor(){
    this.key = 'rdec-key-123-45678-011121314';
    this.weatherURI = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/';
    this.cityURI = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001';
    this.id = 'F-C0032-001';
  }
  async updateCity(city){
    const cityDets = await this.getCity(city);
    const weather = await this.getWeather(city);
    return { cityDets, weather };
  }
  async getCity(city){
    const query = `?Authorization=${this.key}&amp;locationName=${city}`;
    const response = await fetch(this.cityURI + query);
    const data = await response.json();
    return data;
  }
  async getWeather(city){
    const query = `${this.id}?Authorization=${this.key}&amp;locationName=${city}`;
    const response = await fetch(this.weatherURI + query);
    const data = await response.json();
    return data;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// scripts/app.js

const cityForm = document.querySelector('form');
const card = document.querySelector('.card');
const details = document.querySelector('.details');
const time = document.querySelector('img.time');
const icon = document.querySelector('.icon img');
const forecast = new Forecast();

// console.log(forecast);

const updateUI = (data) =&gt; {
  // destructure properties
  const { cityDets, weather } = data;

  // update details template
  details.innerHTML = `
    &lt;h4 class="my-3"&gt;12小時內天氣預報&lt;/h4&gt;
    &lt;h5 class="my-3"&gt;${cityDets.records.location&#91;0].locationName}&lt;/h5&gt;
    &lt;div class="my-3"&gt;${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterName}&lt;/div&gt;
    &lt;div class="display-4 my-4"&gt;
      &lt;span&gt;氣溫&lt;/span&gt;&lt;br&gt;
      &lt;span&gt;${weather.records.location&#91;0].weatherElement&#91;2].time&#91;0].parameter.parameterName} - ${weather.records.location&#91;0].weatherElement&#91;4].time&#91;0].parameter.parameterName}  &amp;deg;C&lt;/span&gt;
    &lt;/div&gt;
  `;

  // update the night/day &amp; icon images
  const iconSrc = `img/icons/${weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].parameter.parameterValue}.svg`;
  icon.setAttribute('src', iconSrc);


  const timeSrc = weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("06:00:00") || weather.records.location&#91;0].weatherElement&#91;0].time&#91;0].startTime.indexOf("12:00:00") &gt;= 0 ? 'img/day.svg' : 'img/night.svg';
  time.setAttribute('src', timeSrc);

  // remove the d-none class if present
  if(card.classList.contains('d-none')){
    card.classList.remove('d-none');
  }

};

cityForm.addEventListener('submit', e =&gt; {
  // prevent default action
  e.preventDefault();

  // get city value
  const city = cityForm.city.value.trim();
  cityForm.reset();

  // update the ui with new city
  forecast.updateCity(city)
  .then(data =&gt; updateUI(data))
  .catch(err =&gt; console.log(err));

  // set local storage
  localStorage.setItem('city', city);

});

if(localStorage.getItem('city')){
  forecast.updateCity(localStorage.getItem('city'))
    .then(data =&gt; updateUI(data))
    .catch(err =&gt; console.log(err));
};</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Databases (Firebase)</h2>



<h3 class="wp-block-heading">NoSQL Databases</h3>



<h4 class="wp-block-heading">Storing Data</h4>



<ul class="wp-block-list"><li>Websites work with data<ul><li>blog posts, todos, comments, user info, scores etc</li></ul></li><li>We can store this data in database<ul><li>Firebase (by Google)</li></ul></li></ul>



<h4 class="wp-block-heading">Storing Data</h4>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1497" height="725" src="/wordpress_blog/wp-content/uploads/2022/04/Storing-Data.png" alt="" class="wp-image-521"/><figcaption>Storing Data</figcaption></figure>



<h4 class="wp-block-heading">NoSQL Databases</h4>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1536" height="652" src="/wordpress_blog/wp-content/uploads/2022/04/NoSQL-Databases.png" alt="" class="wp-image-522"/><figcaption>NoSQL Databases</figcaption></figure>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Firebase &amp; Firestore</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>GO TO CONSOLE</li><li>新的 Firebase 專案 &gt; 新增專案</li><li>專案名稱 udemy-modern-javascript</li><li>適用於 Firebase 專案的 Google Analytics (分析)<ul><li>這裡我先不使用</li></ul></li><li>建立專案</li><li>建構 &gt; Firestore Database</li><li>建立資料庫</li><li>Cloud Firestore 的安全規則<ul><li>以測試模式啟動 (在這裡先使用這個)<br>以正式版模式啟動 (在未來使用上安全是重要的)</li></ul></li><li>設定 Cloud Firestore 位置<ul><li>選擇東亞啟用</li></ul></li><li>新增集合 (Add collection)<ul><li>集合 ID (Collection ID) – recipes</li></ul></li><li>新增第一份文件 (Add first document)<ul><li>欄位(Field) title、類型(Type) string、值(Value) veg &amp; tofu ninja curry</li><li>欄位(Field) author、類型(Type) string、值(Value) ryu</li><li>欄位(Field) created_at 日期(Date) 2022年1月24日、類型(Type) timestamp</li></ul></li><li>自動產生的 ID、然後儲存</li><li>新增文件 (Add a document)<ul><li>欄位(Field) title、類型(Type) string、值(Value) spring green burrito</li><li>欄位(Field) author、類型(Type) string、值(Value) chun li</li><li>欄位(Field) created_at 日期(Date) 2022年1月25日、類型(Type) timestamp</li></ul></li><li>自動產生的 ID、然後儲存</li></ul>



<p>可以參考 The Net Ninja Youtube Channel 的其他課程。</p>



<h4 class="wp-block-heading">課程補充文件</h4>



<ul class="wp-block-list"><li><a href="https://firebase.google.com/docs/firestore/quickstart#set_up_your_development_environment" target="_blank" rel="noreferrer noopener">Firebase Documentation 開始使用 Cloud Firestore</a></li></ul>



<pre class="wp-block-code"><code>// In index.html file put the script tag like this:

&lt;script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"&gt;&lt;/script&gt;
&lt;script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-firestore.js"&gt;&lt;/script&gt;
&lt;script&gt;
  // replace "xxxx" with your own generated firebase config
  const firebaseConfig = {
  apiKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  authDomain: 'xxxxxxxxxxxxxxxxxxxxx.com',
  projectId: 'xxxxx-js',
  storageBucket: 'xxxxxxxxxxx.com',
  messagingSenderId: 'xxxxxxxxxxxxxxxxx',
  appId: 'xxxxxxxxxxxxxxxxx',
  measurementId: 'xxxxxxxxxxxx'
  };
    
  firebase.initializeApp(firebaseConfig);
  const db = firebase.firestore();
&lt;/script&gt;
&lt;script src="sandbox.js"&gt;&lt;/script&gt;</code></pre>



<pre class="wp-block-code"><code>// in sandbox.js file

db.collection('recipes')
   .get()
   .then((snapshot) =&gt; {
      snapshot.docs.forEach((doc) =&gt; {
         console.log(doc.data());
      });
   })
   .catch((err) =&gt; {
      console.log(err);
   });</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Connecting to Firestore</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Databases(Firebase)&lt;/h1&gt;
    
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>建立一個 index.html 檔案</li><li>專案總覽 udemy-modern-javascript</li><li>首先請新增應用程式 &gt; 選擇網頁</li><li>將 Firebase 新增至您的網頁應用程式<ul><li>註冊應用程式 &gt;應用程式暱稱 udemy-modern-javascript &gt; 註冊應用程式</li><li>新增 Firebase SDK &gt; 使用 &lt;script&gt; 標記 (選擇這個)</li></ul></li><li>這個教學影片版本是使用 5.9.1</li><li>載入 Bootstrap v4.6</li><li>製作 index.html 的 HTML Template</li></ul>



<p>注意:<br>Make sure you use the same version of Firebase as me – 5.9.1 – to make sure everything works the same way as in the videos.<br><br>There will be a chapter at the end of the course showing how to use the most recent version of Firebase as well (version 9)</p>



<pre class="wp-block-code"><code>// index.html - HTML Template

  &lt;div class="container my-5"&gt;
    &lt;h2&gt;Recipes&lt;/h2&gt;
    &lt;ul&gt;
      &lt;li&gt;Mushroom pie&lt;/li&gt;
      &lt;li&gt;Veg curry&lt;/li&gt;
    &lt;/ul&gt;
    &lt;form&gt;
      &lt;label for="recipe"&gt;Add a new recipe:&lt;/label&gt;
      &lt;div class="input-group"&gt;
        &lt;input type="text" class="form-control" id="recipe" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" value="add" class="btn btn-outline-secondary"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;</code></pre>



<pre class="wp-block-code"><code>// index.html - 串接資料查閱 Firebase

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
  &lt;div class="container my-5"&gt;
    &lt;h2&gt;Recipes&lt;/h2&gt;
    &lt;ul&gt;
      &lt;li&gt;Mushroom pie&lt;/li&gt;
      &lt;li&gt;Veg curry&lt;/li&gt;
    &lt;/ul&gt;
    &lt;form&gt;
      &lt;label for="recipe"&gt;Add a new recipe:&lt;/label&gt;
      &lt;div class="input-group"&gt;
        &lt;input type="text" class="form-control" id="recipe" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" value="add" class="btn btn-outline-secondary"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
    
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"&gt;&lt;/script&gt;
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"&gt;&lt;/script&gt;
    &lt;script&gt;
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    &lt;/script&gt;
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Getting Collections</h3>



<pre class="wp-block-code"><code>// sandbox.js

</code></pre>



<pre class="wp-block-code"><code>// index.html - 串接資料查閱 firebase

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
  &lt;div class="container my-5"&gt;
    &lt;h2&gt;Recipes&lt;/h2&gt;
    &lt;ul&gt;
    &lt;/ul&gt;
    &lt;form&gt;
      &lt;label for="recipe"&gt;Add a new recipe:&lt;/label&gt;
      &lt;div class="input-group"&gt;
        &lt;input type="text" class="form-control" id="recipe" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" value="add" class="btn btn-outline-secondary"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
    
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"&gt;&lt;/script&gt;
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"&gt;&lt;/script&gt;
    &lt;script&gt;
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    &lt;/script&gt;
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const list = document.querySelector('ul');

const addRecipe = (recipe) =&gt; {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    &lt;li&gt;
      &lt;div&gt;${recipe.title}&lt;/div&gt;
      &lt;div&gt;${time}&lt;/div&gt;
    &lt;/li&gt;
  `;

  // console.log(html);
  list.innerHTML += html;
}

// get documents
db.collection('recipes').get().then(snapshot =&gt; {
  // when we have the data
  snapshot.docs.forEach(doc =&gt; {
    // console.log(doc.data());
    addRecipe(doc.data());
  });
}).catch(err =&gt; {
  console.log(err);
});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Saving Documents</h3>



<pre class="wp-block-code"><code>// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');

const addRecipe = (recipe) =&gt; {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    &lt;li&gt;
      &lt;div&gt;${recipe.title}&lt;/div&gt;
      &lt;div&gt;${time}&lt;/div&gt;
    &lt;/li&gt;
  `;

  // console.log(html);
  list.innerHTML += html;
}

// get documents
db.collection('recipes').get().then(snapshot =&gt; {
  // when we have the data
  snapshot.docs.forEach(doc =&gt; {
    // console.log(doc.data());
    addRecipe(doc.data());
  });
}).catch(err =&gt; {
  console.log(err);
});

// add documents
form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() =&gt; {
    console.log('recipe added');
  }).catch(err =&gt; {
    console.log(err);
  });

});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Deleting Documents</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// index.html - 串接資料查閱 Firebase

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
  &lt;div class="container my-5"&gt;
    &lt;h2&gt;Recipes&lt;/h2&gt;
    &lt;ul&gt;
    &lt;/ul&gt;
    &lt;form&gt;
      &lt;label for="recipe"&gt;Add a new recipe:&lt;/label&gt;
      &lt;div class="input-group"&gt;
        &lt;input type="text" class="form-control" id="recipe" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" value="add" class="btn btn-outline-secondary"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  &lt;/div&gt;
    
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"&gt;&lt;/script&gt;
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"&gt;&lt;/script&gt;
    &lt;script&gt;
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    &lt;/script&gt;
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');

const addRecipe = (recipe, id) =&gt; {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    &lt;li data-id="${id}"&gt;
      &lt;div&gt;${recipe.title}&lt;/div&gt;
      &lt;div&gt;${time}&lt;/div&gt;
      &lt;button class="btn btn-danger btn-sm my-2"&gt;delete&lt;/button&gt;
    &lt;/li&gt;
  `;

  // console.log(html);
  list.innerHTML += html;
}

// get documents
db.collection('recipes').get().then(snapshot =&gt; {
  // when we have the data
  snapshot.docs.forEach(doc =&gt; {
    // console.log(doc.data());
    // console.log(doc.id);
    addRecipe(doc.data(), doc.id);
  });
}).catch(err =&gt; {
  console.log(err);
});

// add documents
form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() =&gt; {
    console.log('recipe added');
  }).catch(err =&gt; {
    console.log(err);
  });

});

// deleting data
list.addEventListener('click', e =&gt; {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    const id = e.target.parentElement.getAttribute('data-id');
    // console.log(id);
    db.collection('recipes').doc(id).delete().then(() =&gt; {
      console.log('recipe deleted');
    });
  }
});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Real-time Listeners</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');

const addRecipe = (recipe, id) =&gt; {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    &lt;li data-id="${id}"&gt;
      &lt;div&gt;${recipe.title}&lt;/div&gt;
      &lt;div&gt;${time}&lt;/div&gt;
      &lt;button class="btn btn-danger btn-sm my-2"&gt;delete&lt;/button&gt;
    &lt;/li&gt;
  `;

  // console.log(html);
  list.innerHTML += html;
}

const deleteRecipe = (id) =&gt; {
  const recipes = document.querySelectorAll('li');
  recipes.forEach(recipe =&gt; {
    if(recipe.getAttribute('data-id') === id){
      recipe.remove();
    }
  });
}

// get documents
db.collection('recipes').onSnapshot(snapshot =&gt; {
  // console.log(snapshot.docChanges());
  snapshot.docChanges().forEach(change =&gt; {
    // console.log(change);
    const doc = change.doc;
    // console.log(doc);
    if(change.type === 'added'){
      addRecipe(doc.data(), doc.id);
    } else if (change.type === 'removed'){
      deleteRecipe(doc.id);
    }
  })
});

// add documents
form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() =&gt; {
    console.log('recipe added');
  }).catch(err =&gt; {
    console.log(err);
  });

});

// deleting data
list.addEventListener('click', e =&gt; {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    const id = e.target.parentElement.getAttribute('data-id');
    // console.log(id);
    db.collection('recipes').doc(id).delete().then(() =&gt; {
      console.log('recipe deleted');
    });
  }
});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Unsubscribing</h3>



<pre class="wp-block-code"><code>// index.html - 串接資料查閱 Firebase

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
  &lt;div class="container my-5"&gt;
    &lt;h2&gt;Recipes&lt;/h2&gt;
    &lt;ul&gt;
    &lt;/ul&gt;
    &lt;form&gt;
      &lt;label for="recipe"&gt;Add a new recipe:&lt;/label&gt;
      &lt;div class="input-group"&gt;
        &lt;input type="text" class="form-control" id="recipe" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" value="add" class="btn btn-outline-secondary"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;
    &lt;button&gt;unsubscribe from changes&lt;/button&gt;
  &lt;/div&gt;
    
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"&gt;&lt;/script&gt;
    &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"&gt;&lt;/script&gt;
    &lt;script&gt;
      // Initialize Firebase
      const firebaseConfig = {
          apiKey: "",
          authDomain: "",
          projectId: "",
          storageBucket: "",
          messagingSenderId: "",
          appId: ""
      };
      
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
      const db = firebase.firestore();
    &lt;/script&gt;
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const list = document.querySelector('ul');
const form = document.querySelector('form');
const button = document.querySelector('button');

const addRecipe = (recipe, id) =&gt; {
  // console.log(recipe.created_at.toDate());
  let time = recipe.created_at.toDate();
  let html = `
    &lt;li data-id="${id}"&gt;
      &lt;div&gt;${recipe.title}&lt;/div&gt;
      &lt;div&gt;${time}&lt;/div&gt;
      &lt;button class="btn btn-danger btn-sm my-2"&gt;delete&lt;/button&gt;
    &lt;/li&gt;
  `;

  // console.log(html);
  list.innerHTML += html;
}

const deleteRecipe = (id) =&gt; {
  const recipes = document.querySelectorAll('li');
  recipes.forEach(recipe =&gt; {
    if(recipe.getAttribute('data-id') === id){
      recipe.remove();
    }
  });
}

// get documents
const unsub = db.collection('recipes').onSnapshot(snapshot =&gt; {
  // console.log(snapshot.docChanges());
  snapshot.docChanges().forEach(change =&gt; {
    // console.log(change);
    const doc = change.doc;
    // console.log(doc);
    if(change.type === 'added'){
      addRecipe(doc.data(), doc.id);
    } else if (change.type === 'removed'){
      deleteRecipe(doc.id);
    }
  })
});

// add documents
form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  const now = new Date();
  const recipe = {
    title: form.recipe.value,
    created_at: firebase.firestore.Timestamp.fromDate(now)
  };

  db.collection('recipes').add(recipe).then(() =&gt; {
    console.log('recipe added');
  }).catch(err =&gt; {
    console.log(err);
  });

});

// deleting data
list.addEventListener('click', e =&gt; {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    const id = e.target.parentElement.getAttribute('data-id');
    // console.log(id);
    db.collection('recipes').doc(id).delete().then(() =&gt; {
      console.log('recipe deleted');
    });
  }
});

// unsub from database changes
button.addEventListener('click', () =&gt; {
  unsub();
  console.log('unsubscribed from collection changes');
});</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Project – Real-time Chatroom</h2>



<p>非常有趣，不懂可以重複觀看、練習。</p>



<h3 class="wp-block-heading">Project Preview &amp; Setup</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>建立一個專案資料夾 ninja_chat</li><li>建立 index.html 檔案</li><li>建立一個 scripts 資料夾</li><li>在 scripts 資料夾裡面建立以下檔案<ul><li>app.js</li><li>ui.js</li><li>chat.js</li></ul></li><li>載入 JS 檔案，chat.js、ui.js、app.js</li><li>載入 CSS 檔案，bootstrap v4.6 cdn</li><li>建立 style.css 檔案</li><li>載入 CSS 檔案，style.css</li><li>開啟 Live Server</li></ul>



<h3 class="wp-block-heading">HTML Template</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Chat&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;!-- container &amp; title --&gt;
  &lt;div class="container my-4"&gt;
    &lt;h1 class="my-4 text-center"&gt;Ninja Chat&lt;/h1&gt;


    &lt;!-- buttons for chatrooms --&gt;
    &lt;div class="chat-rooms mb-3 text-center"&gt;
      &lt;div class="my-2"&gt;Choose a chatroom:&lt;/div&gt;
      &lt;button class="btn" id="general"&gt;#general&lt;/button&gt;
      &lt;button class="btn" id="gaming"&gt;#gaming&lt;/button&gt;
      &lt;button class="btn" id="music"&gt;#music&lt;/button&gt;
      &lt;button class="btn" id="ninjas"&gt;#ninjas&lt;/button&gt;
    &lt;/div&gt;

    &lt;!-- chat list / window --&gt;
    &lt;div class="chat-window"&gt;
      &lt;ul class="chat-list list-group"&gt;&lt;/ul&gt;
    &lt;/div&gt;

    &lt;!-- new chat form --&gt;
    &lt;form class="new-chat my-3"&gt;
      &lt;div class="input-group"&gt;
        &lt;div class="input-group-prepend"&gt;
          &lt;div class="input-group-text"&gt;Your message:&lt;/div&gt;
        &lt;/div&gt;
        &lt;input type="text" id="message" class="form-control" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" class="btn" value="send"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;

    &lt;!-- update name form --&gt;
    &lt;form class="new-name my-3"&gt;
      &lt;div class="input-group"&gt;
        &lt;div class="input-group-prepend"&gt;
          &lt;div class="input-group-text"&gt;Update name:&lt;/div&gt;
        &lt;/div&gt;
        &lt;input type="text" id="name" class="form-control" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" class="btn" value="update"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="update-mssg"&gt;&lt;/div&gt;
    &lt;/form&gt;
    
  &lt;/div&gt;

  &lt;script src="scripts/chat.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/ui.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

.container{
  max-width: 600px;
}
.btn{
  background: #43d9be;
  color: white;
  outline: none !important;
  box-shadow: none !important;
}
.btn:focus{
  outline: none !important;
}</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Connecting to Firebase</h3>



<p>Make sure you use the same version of Firebase as me – 5.9.1 – to make sure everything works the same way as in the videos.<br><br>There will be a chapter at the end of the course showing how to use the most recent version of Firebase as well (version 9)</p>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>載入 SDK 設定和配置 (使用 udermy-modern-javascript 來練習)</li><li>點選 udemy-modern-javascript、選擇 Firestore Database</li><li>並不是每次都需要手動新增集合(collection)，這邊是想要新增兩個虛假的文件</li><li>開始新增集合 (Set collection ID)<ul><li>集合ID (Collection ID) chats</li></ul></li><li>新增第一份文件、自動產生 ID，然後儲存<ul><li>欄位(Field) message、類型(Types) string、值(Value) hey guys</li><li>欄位(Field) username、類型(Types) string、值(Value) yoshi</li><li>欄位(Field) room、類型(Types) string、值(Value) general</li><li>欄位(Field) created_at、類型(Types) timestamp 2022年1月27日</li></ul></li><li>新增文件、自動產生 ID、然後儲存<ul><li>欄位(Field) message、類型(Types) string、值(Value) Yo Yoshi</li><li>欄位(Field) username、類型(Types) string、值(Value) mario</li><li>欄位(Field) room、類型(Types) string、值(Value) general</li><li>欄位(Field) created_at、類型 timestamp 2022年1月27日</li></ul></li></ul>



<pre class="wp-block-code"><code>// index.html 串接資料查閱 Firebase

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Chat&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;!-- container &amp; title --&gt;
  &lt;div class="container my-4"&gt;
    &lt;h1 class="my-4 text-center"&gt;Ninja Chat&lt;/h1&gt;


    &lt;!-- buttons for chatrooms --&gt;
    &lt;div class="chat-rooms mb-3 text-center"&gt;
      &lt;div class="my-2"&gt;Choose a chatroom:&lt;/div&gt;
      &lt;button class="btn" id="general"&gt;#general&lt;/button&gt;
      &lt;button class="btn" id="gaming"&gt;#gaming&lt;/button&gt;
      &lt;button class="btn" id="music"&gt;#music&lt;/button&gt;
      &lt;button class="btn" id="ninjas"&gt;#ninjas&lt;/button&gt;
    &lt;/div&gt;

    &lt;!-- chat list / window --&gt;
    &lt;div class="chat-window"&gt;
      &lt;ul class="chat-list list-group"&gt;&lt;/ul&gt;
    &lt;/div&gt;

    &lt;!-- new chat form --&gt;
    &lt;form class="new-chat my-3"&gt;
      &lt;div class="input-group"&gt;
        &lt;div class="input-group-prepend"&gt;
          &lt;div class="input-group-text"&gt;Your message:&lt;/div&gt;
        &lt;/div&gt;
        &lt;input type="text" id="message" class="form-control" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" class="btn" value="send"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;

    &lt;!-- update name form --&gt;
    &lt;form class="new-name my-3"&gt;
      &lt;div class="input-group"&gt;
        &lt;div class="input-group-prepend"&gt;
          &lt;div class="input-group-text"&gt;Update name:&lt;/div&gt;
        &lt;/div&gt;
        &lt;input type="text" id="name" class="form-control" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" class="btn" value="update"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="update-mssg"&gt;&lt;/div&gt;
    &lt;/form&gt;
    
  &lt;/div&gt;

  &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"&gt;&lt;/script&gt;
  &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"&gt;&lt;/script&gt;
  &lt;script&gt;
    // Initialize Firebase
    const firebaseConfig = {
        apiKey: "",
        authDomain: "",
        projectId: "",
        storageBucket: "",
        messagingSenderId: "",
        appId: ""
    };
    
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();
  &lt;/script&gt;
  &lt;script src="scripts/chat.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/ui.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Chatroom Class &amp; Adding Chats</h3>



<p>chat.js 從零開始撰寫程式碼，<br>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
}

const chatroom = new Chatroom('gaming', 'shaun');
// console.log(chatroom);

chatroom.addChat('hello everyone')
  .then(() =&gt; console.log('chat added'))
  .catch(err =&gt; console.log(err));</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Setting up a Real-time Listener</h3>



<pre class="wp-block-code"><code>// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.chats
      .onSnapshot(snapshot =&gt; {
        snapshot.docChanges().forEach(change =&gt; {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
}

const chatroom = new Chatroom('gaming', 'shaun');

chatroom.getChats((data) =&gt; {
  console.log(data);
});</code></pre>



<h4 class="wp-block-heading">測試 – 在 Firestore Database 新增文件是否會 console.log 出物件資料</h4>



<ul class="wp-block-list"><li>新增文件<ul><li>欄位(Field) message、類型(Types) string、值(Value) test</li></ul></li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Complex Queries</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot =&gt; {
        snapshot.docChanges().forEach(change =&gt; {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
}

const chatroom = new Chatroom('general', 'shaun');

chatroom.getChats((data) =&gt; {
  console.log(data);
});</code></pre>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>在 Firestore Database 移除 test 文件</li><li>在 orderBy(‘created_at) 產生錯誤時 Google Console 有顯示如何建立 index、開啟後按建立</li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Updating the Room &amp; Username</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
    this.unsub;
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.unsub = this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot =&gt; {
        snapshot.docChanges().forEach(change =&gt; {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
  updateName(username){
    this.username = username;
  }
  updateRoom(room){
    this.room = room;
    console.log('room updated');
    if(this.undub){
      this.unsub();
    }
  }
}

const chatroom = new Chatroom('general', 'shaun');

chatroom.getChats((data) =&gt; {
  console.log(data);
});

setTimeout(() =&gt; {
  chatroom.updateRoom('gaming');
  chatroom.updateName('yoshi');
  chatroom.getChats((data) =&gt; {
    console.log(data);
  });
  chatroom.addChat('hello');
}, 3000);</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Creating a ChatUI Class</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
    this.unsub;
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.unsub = this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot =&gt; {
        snapshot.docChanges().forEach(change =&gt; {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
  updateName(username){
    this.username = username;
  }
  updateRoom(room){
    this.room = room;
    console.log('room updated');
    if(this.undub){
      this.unsub();
    }
  }
}

</code></pre>



<pre class="wp-block-code"><code>// app.js

// dom queries
const chatList = document.querySelector('.chat-list');

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', 'shaun');

// get chats and render
chatroom.getChats(data =&gt; chatUI.render(data));</code></pre>



<pre class="wp-block-code"><code>// ui.js

// render chat templates to the DOM
// clear the list of chats (when the room changes)

class ChatUI {
  constructor(list){
    this.list = list;
  }
  render(data){
    const html = `
      &lt;li class="list-group-item"&gt;
        &lt;span class="username"&gt;${data.username}&lt;/span&gt;
        &lt;span class="message"&gt;${data.message}&lt;/span&gt;
        &lt;div class="time"&gt;${data.created_at.toDate()}&lt;/div&gt;
      &lt;/li&gt;
    `;

    this.list.innerHTML += html;
  }
}
</code></pre>



<h3 class="wp-block-heading">Formatting the Dates</h3>



<h4 class="wp-block-heading">GitHub Course</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://github.com/iamshaunjp/modern-javascript/blob/lesson-143/chat_project/index.html" target="_blank">課程連結</a></li><li><a href="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.9.0/date_fns.min.js" target="_blank" rel="noreferrer noopener">自己找的 date-fns 1.9.0 cdn</a></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Ninja Chat&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

  &lt;!-- container &amp; title --&gt;
  &lt;div class="container my-4"&gt;
    &lt;h1 class="my-4 text-center"&gt;Ninja Chat&lt;/h1&gt;


    &lt;!-- buttons for chatrooms --&gt;
    &lt;div class="chat-rooms mb-3 text-center"&gt;
      &lt;div class="my-2"&gt;Choose a chatroom:&lt;/div&gt;
      &lt;button class="btn" id="general"&gt;#general&lt;/button&gt;
      &lt;button class="btn" id="gaming"&gt;#gaming&lt;/button&gt;
      &lt;button class="btn" id="music"&gt;#music&lt;/button&gt;
      &lt;button class="btn" id="ninjas"&gt;#ninjas&lt;/button&gt;
    &lt;/div&gt;

    &lt;!-- chat list / window --&gt;
    &lt;div class="chat-window"&gt;
      &lt;ul class="chat-list list-group"&gt;&lt;/ul&gt;
    &lt;/div&gt;

    &lt;!-- new chat form --&gt;
    &lt;form class="new-chat my-3"&gt;
      &lt;div class="input-group"&gt;
        &lt;div class="input-group-prepend"&gt;
          &lt;div class="input-group-text"&gt;Your message:&lt;/div&gt;
        &lt;/div&gt;
        &lt;input type="text" id="message" class="form-control" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" class="btn" value="send"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/form&gt;

    &lt;!-- update name form --&gt;
    &lt;form class="new-name my-3"&gt;
      &lt;div class="input-group"&gt;
        &lt;div class="input-group-prepend"&gt;
          &lt;div class="input-group-text"&gt;Update name:&lt;/div&gt;
        &lt;/div&gt;
        &lt;input type="text" id="name" class="form-control" required&gt;
        &lt;div class="input-group-append"&gt;
          &lt;input type="submit" class="btn" value="update"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="update-mssg"&gt;&lt;/div&gt;
    &lt;/form&gt;
    
  &lt;/div&gt;

  &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.9.0/date_fns.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-app.js"&gt;&lt;/script&gt;
  &lt;script src="https://www.gstatic.com/firebasejs/5.9.1/firebase-firestore.js"&gt;&lt;/script&gt;
  &lt;script&gt;
    // Initialize Firebase
    const firebaseConfig = {
        apiKey: "",
        authDomain: "",
        projectId: "",
        storageBucket: "",
        messagingSenderId: "",
        appId: ""
    };
    
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();
  &lt;/script&gt;
  &lt;script src="scripts/chat.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/ui.js"&gt;&lt;/script&gt;
  &lt;script src="scripts/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// ui.js

// render chat templates to the DOM
// clear the list of chats (when the room changes)

class ChatUI {
  constructor(list){
    this.list = list;
  }
  render(data){
    const when = dateFns.distanceInWordsToNow(
      data.created_at.toDate(),
      { addSuffix: true }
    );
    const html = `
      &lt;li class="list-group-item"&gt;
        &lt;span class="username"&gt;${data.username}&lt;/span&gt;
        &lt;span class="message"&gt;${data.message}&lt;/span&gt;
        &lt;div class="time"&gt;${when}&lt;/div&gt;
      &lt;/li&gt;
    `;

    this.list.innerHTML += html;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// style.css

.container{
  max-width: 600px;
}
.btn{
  background: #43d9be;
  color: white;
  outline: none !important;
  box-shadow: none !important;
}
.btn:focus{
  outline: none !important;
}
.username{
  font-weight: bold;
}
.time{
  font-size: 0.7em;
  color: #999;
}</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Sending New Chats</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// app.js

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');

// add a new chat
newChatForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() =&gt; newChatForm.reset())
    .catch(err =&gt; console.log(err));
});

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', 'shaun');

// get chats and render
chatroom.getChats(data =&gt; chatUI.render(data));</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Changing Username &amp; Local Storage</h3>



<p>重要，不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// app.js - 1

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');
const newNameForm = document.querySelector('.new-name');
const updateMssg = document.querySelector('.update-mssg');

// add a new chat
newChatForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() =&gt; newChatForm.reset())
    .catch(err =&gt; console.log(err));
});

// update username
newNameForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // update name via chatroom
  const newName = newNameForm.name.value.trim();
  chatroom.updateName(newName);
  // reset the form
  newNameForm.reset();
  // show then hide the update message
  updateMssg.innerText = `Your name was updated to ${newName}`;
  setTimeout(() =&gt; updateMssg.innerText = '', 3000);
});

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', 'shaun');

// get chats and render
chatroom.getChats(data =&gt; chatUI.render(data));</code></pre>



<pre class="wp-block-code"><code>// style.css

.container{
  max-width: 600px;
}
.btn{
  background: #43d9be;
  color: white;
  outline: none !important;
  box-shadow: none !important;
}
.btn:focus{
  outline: none !important;
}
.username{
  font-weight: bold;
}
.time{
  font-size: 0.7em;
  color: #999;
}
.update-mssg{
  text-align: center;
  margin: 20px auto;
}</code></pre>



<pre class="wp-block-code"><code>// Google Console
&gt;  chatroom
&lt;  Chatroom&nbsp;{room: 'general', username: 'luigi', chats: t, unsub: ƒ}</code></pre>



<pre class="wp-block-code"><code>// chat.js

// adding new chat documents
// setting up a real-time listener to get new chats
// updating the username
// updating the room

class Chatroom {
  constructor(room, username){
    this.room = room;
    this.username = username;
    this.chats = db.collection('chats');
    this.unsub;
  }
  async addChat(message){
    // format a chat object
    const now = new Date();
    const chat = {
      message,
      username: this.username,
      room: this.room,
      created_at: firebase.firestore.Timestamp.fromDate(now)
    };
    // save the chat document
    const response = await this.chats.add(chat);
    return response;
  }
  getChats(callback){
    this.unsub = this.chats
      .where('room', '==', this.room)
      .orderBy('created_at')
      .onSnapshot(snapshot =&gt; {
        snapshot.docChanges().forEach(change =&gt; {
          if(change.type === 'added'){
            // update the ui
            callback(change.doc.data());
          }
        });
    });
  }
  updateName(username){
    this.username = username;
    localStorage.setItem('username', username);
  }
  updateRoom(room){
    this.room = room;
    console.log('room updated');
    if(this.undub){
      this.unsub();
    }
  }
}

</code></pre>



<pre class="wp-block-code"><code>// app.js - 2

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');
const newNameForm = document.querySelector('.new-name');
const updateMssg = document.querySelector('.update-mssg');

// add a new chat
newChatForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() =&gt; newChatForm.reset())
    .catch(err =&gt; console.log(err));
});

// update username
newNameForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // update name via chatroom
  const newName = newNameForm.name.value.trim();
  chatroom.updateName(newName);
  // reset the form
  newNameForm.reset();
  // show then hide the update message
  updateMssg.innerText = `Your name was updated to ${newName}`;
  setTimeout(() =&gt; updateMssg.innerText = '', 3000);
});

// check local storage for a name
const username = localStorage.username ? localStorage.username : 'anon';

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', username);

// get chats and render
chatroom.getChats(data =&gt; chatUI.render(data));</code></pre>



<pre class="wp-block-code"><code>// Google Console - test
&gt;  chatroom
&lt;  Chatroom&nbsp;{room: 'general', username: 'anon', chats: t, unsub: ƒ}
&gt;</code></pre>



<pre class="wp-block-code"><code>// Goolge Console - Update name: luigi
&gt;  chatroom
&lt;  Chatroom&nbsp;{room: 'general', username: 'anon', chats: t, unsub: ƒ}
&gt;  chatroom
&lt;  Chatroom&nbsp;{room: 'general', username: 'luigi', chats: t, unsub: ƒ}
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Updating the Room</h3>



<p>不懂可重複觀看、練習。</p>



<pre class="wp-block-code"><code>// app.js

// dom queries
const chatList = document.querySelector('.chat-list');
const newChatForm = document.querySelector('.new-chat');
const newNameForm = document.querySelector('.new-name');
const updateMssg = document.querySelector('.update-mssg');
const rooms = document.querySelector('.chat-rooms');

// add a new chat
newChatForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  const message = newChatForm.message.value.trim();
  chatroom.addChat(message)
    .then(() =&gt; newChatForm.reset())
    .catch(err =&gt; console.log(err));
});

// update username
newNameForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // update name via chatroom
  const newName = newNameForm.name.value.trim();
  chatroom.updateName(newName);
  // reset the form
  newNameForm.reset();
  // show then hide the update message
  updateMssg.innerText = `Your name was updated to ${newName}`;
  setTimeout(() =&gt; updateMssg.innerText = '', 3000);
});

// update the chat room
rooms.addEventListener('click', e =&gt; {
  // console.log(e);
  if(e.target.tagName === 'BUTTON'){
    chatUI.clear();
    chatroom.updateRoom(e.target.getAttribute('id'));
    chatroom.getChats(chat =&gt; chatUI.render(chat));
  }
});

// check local storage for a name
const username = localStorage.username ? localStorage.username : 'anon';

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', username);

// get chats and render
chatroom.getChats(data =&gt; chatUI.render(data));</code></pre>



<pre class="wp-block-code"><code>// ui.js

// render chat templates to the DOM
// clear the list of chats (when the room changes)

class ChatUI {
  constructor(list){
    this.list = list;
  }
  clear(){
    this.list.innerHTML = '';
  }
  render(data){
    const when = dateFns.distanceInWordsToNow(
      data.created_at.toDate(),
      { addSuffix: true }
    );
    const html = `
      &lt;li class="list-group-item"&gt;
        &lt;span class="username"&gt;${data.username}&lt;/span&gt;
        &lt;span class="message"&gt;${data.message}&lt;/span&gt;
        &lt;div class="time"&gt;${when}&lt;/div&gt;
      &lt;/li&gt;
    `;

    this.list.innerHTML += html;
  }
}
</code></pre>



<h3 class="wp-block-heading">Testing the App</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://www.youtube.com/playlist?list=PL4cUxeGkcC9jUPIes_B8vRjn1_GaplOPQ" target="_blank" rel="noreferrer noopener">Firebase Auth Playlist on The Net Ninja YouTube Channel</a></li></ul>



<h2 class="wp-block-heading">More ES6 Features</h2>



<h3 class="wp-block-heading">Spread &amp; Rest</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

// rest parameter - 其餘參數
const double = (...nums) =&gt; {
  console.log(nums);
  return nums.map(num =&gt; num*2);
}

const result = double(1,3,5,7,9,2,4,6,8);
console.log(result);

// spread syntax (arrays) - 展開語法 (陣列)

const people = &#91;'shaun', 'ryu', 'crystal'];
console.log(...people);
const members = &#91;'mario', 'chun-li', ...people];
console.log(members);

// spread syntax (objects) - 展開語法 (物件)

const person = { name: 'shaun', age: 30, job: 'net ninja'};
const personClone = {...person, location: 'manchester'};

console.log(personClone);</code></pre>



<h3 class="wp-block-heading">Sets</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

// sets
const namesArray = &#91;'ryu', 'chun-li', 'ryu', 'shaun'];
console.log(namesArray);

// const namesSet = new Set(&#91;'ryu', 'chun-li', 'ryu', 'shaun']);
// const namesSet = new Set(namesArray);
// console.log(namesSet);

// const uniqueNames = &#91;...namesSet];
const uniqueNames = &#91;...new Set(namesArray)];
console.log(uniqueNames);

const ages = new Set();
ages.add(20);
ages.add(25).add(30);
ages.add(25);
ages.delete(25);

console.log(ages, ages.size);
console.log(ages.has(30), ages.has(25));

ages.clear();
console.log(ages);

const ninjas = new Set(&#91;
  {name: 'shaun', age: 30},
  {name: 'crystal', age: 29},
  {name: 'chun-li', age: 32}
]);

ninjas.forEach(ninja =&gt; {
  console.log(ninja.name, ninja.age);
});</code></pre>



<h3 class="wp-block-heading">Symbols (符號)</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const symbolOne = Symbol('a generic name');
const symbolTwo = Symbol('a generic name');

console.log(symbolOne, symbolTwo, typeof symbolOne);
console.log(symbolOne === symbolTwo);

const ninja = {};

ninja.age = 30;
ninja&#91;'belt'] = 'orange';
ninja&#91;'belt'] = 'black';

ninja&#91;symbolOne] = 'ryu';
ninja&#91;symbolTwo] = 'shaun';

console.log(ninja);</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Modern JavaScript (2)</title>
		<link>/wordpress_blog/modern-javascript-2/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Mon, 17 Jan 2022 03:46:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=492</guid>

					<description><![CDATA[(Complete guide, from Novice to  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>(Complete guide, from Novice to Ninja)</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Learning Udemy Course：<a href="https://www.udemy.com/course/modern-javascript-from-novice-to-ninja/" target="_blank" rel="noreferrer noopener">Modern JavaScript</a></p><cite>建立者：The Net Ninja (Shaun Pelling)</cite></blockquote>



<p>Learn Modern JavaScript from the very start to ninja-level &amp; build awesome JavaScript applications.</p>



<h4 class="wp-block-heading">您會學到</h4>



<ul class="wp-block-list"><li>Learn how to program with modern JavaScript, from the very beginning to more advanced topics</li><li>Learn all about OOP (object-oriented programming) with JavaScript, working with prototypes &amp; classes</li><li>Learn how to create real-world front-end applications with JavaScript (quizes, weather apps, chat rooms etc)</li><li>Learn how to make useful JavaScript driven UI components like popups, drop-downs, tabs, tool-tips &amp; more.</li><li>Learn how to use modern, cutting-edge JavaScript features today by using a modern workflow (Babel &amp; Webpack)</li><li>Learn how to use real-time databases to store, retrieve and update application data</li><li>Explore API’s to make the most of third-party data (such as weather information)</li></ul>



<h2 class="wp-block-heading">Forms &amp; Form Events</h2>



<h3 class="wp-block-heading">Events Inside Forms</h3>



<h4 class="wp-block-heading">Form Events</h4>



<ul class="wp-block-list"><li>Capture data or information from a user</li><li>E.g. a username &amp; password</li></ul>



<h4 class="wp-block-heading">Submit Events</h4>



<ul class="wp-block-list"><li>“submit” event, which submits the form</li><li>keyboard events</li></ul>



<h3 class="wp-block-heading">Submit Events</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
    &lt;style&gt;
        body {
            background: #eee;
        }

        form {
            max-width: 200px;
            margin: 40px auto;
            background: white;
            padding: 10px;
        }

        input {
            display: block;
            margin: 10px auto;
            padding: 4px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;form class="signup-form"&gt;
        &lt;input type="text" id="username" placeholder="username"&gt;
        &lt;input type="submit" value="submit"&gt;
    &lt;/form&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const form = document.querySelector('.signup-form');
// const username = document.querySelector('#username');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // console.log(username.value);
  console.log(form.username.value);
});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Regular Expressions (正規表達式)</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://regex101.com/" target="_blank" rel="noreferrer noopener">Regex101 Website</a></li><li><a href="https://www.youtube.com/playlist?list=PL4cUxeGkcC9g6m_6Sld9Q4jzqdqHd2HiD" target="_blank" rel="noreferrer noopener">Regex Playlist on the Net Ninja Youtube Channel</a></li></ul>



<h4 class="wp-block-heading">Regex101 介紹</h4>



<ul class="wp-block-list"><li>ninja</li><li>^ninja$</li><li>^[a-z]$</li><li>^[c-p]$</li><li>^[a-zA-Z]$</li><li>^[a-zA-Z]{6,10}$，大小寫a~z、字數6~10</li><li>^[a-zA-Z0-9]{6,10}$，大小寫a-z、數字0到9、字數6~10</li><li>^.{6,10}$，任何字符字元、字數6~10</li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Testing RegEx Patterns</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
    &lt;style&gt;
        body {
            background: #eee;
        }

        form {
            max-width: 200px;
            margin: 40px auto;
            background: white;
            padding: 10px;
        }

        input {
            display: block;
            margin: 10px auto;
            padding: 4px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;form class="signup-form"&gt;
        &lt;input type="text" id="username" placeholder="username"&gt;
        &lt;input type="submit" value="submit"&gt;
    &lt;/form&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

const form = document.querySelector('.signup-form');
// const username = document.querySelector('#username');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // console.log(username.value);
  console.log(form.username.value);
});

// testing RegEx
const username = 'shaunp123';
const pattern = /^&#91;a-z]{6,}$/;

let result = pattern.test(username);
// console.log(result);

if(result){
  console.log('regex test passed :)');
}
else{
  console.log('regex test failed :(');
}</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

const form = document.querySelector('.signup-form');
// const username = document.querySelector('#username');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // console.log(username.value);
  console.log(form.username.value);
});

// testing RegEx
const username = 'shaunp123';
const pattern = /^&#91;a-z]{6,}$/;

// let result = pattern.test(username);
// // console.log(result);

// if(result){
//   console.log('regex test passed :)');
// }
// else{
//   console.log('regex test failed :(');
// }

let result = username.search(pattern);
console.log(result);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   -1
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3

const form = document.querySelector('.signup-form');
// const username = document.querySelector('#username');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // console.log(username.value);
  console.log(form.username.value);
});

// testing RegEx
const username = 'shaunp';
const pattern = /^&#91;a-z]{6,}$/;

// let result = pattern.test(username);
// // console.log(result);

// if(result){
//   console.log('regex test passed :)');
// }
// else{
//   console.log('regex test failed :(');
// }

let result = username.search(pattern);
console.log(result);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 3
   0
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 4

const form = document.querySelector('.signup-form');
// const username = document.querySelector('#username');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // console.log(username.value);
  console.log(form.username.value);
});

// testing RegEx
const username = '3434shaunp3656';
const pattern = /&#91;a-z]{6,}/;

// let result = pattern.test(username);
// // console.log(result);

// if(result){
//   console.log('regex test passed :)');
// }
// else{
//   console.log('regex test failed :(');
// }

let result = username.search(pattern);
console.log(result);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 4
   4
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 5

const form = document.querySelector('.signup-form');
// const username = document.querySelector('#username');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();
  // console.log(username.value);
  console.log(form.username.value);
});

// testing RegEx
const username = '3434shaunp3656';
const pattern = /^&#91;a-z]{6,}$/;

// let result = pattern.test(username);
// // console.log(result);

// if(result){
//   console.log('regex test passed :)');
// }
// else{
//   console.log('regex test failed :(');
// }

let result = username.search(pattern);
console.log(result);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 5
   -1
&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Basic Form Validation</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
    &lt;style&gt;
        body {
            background: #eee;
        }

        form {
            max-width: 200px;
            margin: 40px auto;
            background: white;
            padding: 10px;
        }

        input {
            display: block;
            margin: 10px auto;
            padding: 4px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;form class="signup-form"&gt;
        &lt;input type="text" id="username" name="username" placeholder="username"&gt;
        &lt;input type="submit" value="submit"&gt;
        &lt;div class="feedback"&gt;&lt;/div&gt;
    &lt;/form&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const form = document.querySelector('.signup-form');
const feedback = document.querySelector('.feedback');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  // validation
  const username = form.username.value;
  const usernamePattern = /^&#91;a-zA-Z]{6,12}$/;

  if(usernamePattern.test(username)){
    // feedback good info
    feedback.textContent = 'that username is valid!';
  }
  else{
    // feedback help info
    feedback.textContent = 'username must contain letters only &amp; be between 6 &amp; 12 characters long';
  }

});
</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Keyboard Events</h3>



<h4 class="wp-block-heading">MDN 文件</h4>



<ul class="wp-block-list"><li><a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#validating_forms_using_javascript" target="_blank" rel="noreferrer noopener">Validating forms using JavaScript</a></li></ul>



<pre class="wp-block-code"><code>// index.html - 1

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
    &lt;style&gt;
        body {
            background: #eee;
        }

        form {
            max-width: 200px;
            margin: 40px auto;
            background: white;
            padding: 10px;
        }

        input {
            display: block;
            margin: 10px auto;
            padding: 4px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;form class="signup-form"&gt;
        &lt;input type="text" id="username" name="username" placeholder="username"&gt;
        &lt;input type="submit" value="submit"&gt;
        &lt;div class="feedback"&gt;&lt;/div&gt;
    &lt;/form&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

const form = document.querySelector('.signup-form');
const feedback = document.querySelector('.feedback');
const usernamePattern = /^&#91;a-zA-Z]{6,12}$/;

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  // validation
  const username = form.username.value;

  if(usernamePattern.test(username)){
    // feedback good info
    feedback.textContent = 'that username is valid!';
  }
  else{
    // feedback help info
    feedback.textContent = 'username must contain letters only &amp; be between 6 &amp; 12 characters long';
  }

});

// live feedback
form.username.addEventListener('keyup', e =&gt; {
  // console.log(e.target.value, form.username.value);
  if(usernamePattern.test(e.target.value)){
    console.log('passed');
  }
  else{
    console.log('failed');
  }
});
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
5  failed
   passed
&gt;</code></pre>



<pre class="wp-block-code"><code>// index.html - 2

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
    &lt;style&gt;
        body {
            background: #eee;
        }

        form {
            max-width: 200px;
            margin: 40px auto;
            background: white;
            padding: 10px;
        }

        input {
            display: block;
            margin: 10px auto;
            padding: 4px;
        }

        .success {
            border: 2px solid limegreen;
        }

        .error {
            border: 2px solid crimson;
        }
        
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;form class="signup-form"&gt;
        &lt;input type="text" id="username" name="username" placeholder="username"&gt;
        &lt;input type="submit" value="submit"&gt;
        &lt;div class="feedback"&gt;&lt;/div&gt;
    &lt;/form&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

const form = document.querySelector('.signup-form');
const feedback = document.querySelector('.feedback');
const usernamePattern = /^&#91;a-zA-Z]{6,12}$/;

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  // validation
  const username = form.username.value;

  if(usernamePattern.test(username)){
    // feedback good info
    feedback.textContent = 'that username is valid!';
  }
  else{
    // feedback help info
    feedback.textContent = 'username must contain letters only &amp; be between 6 &amp; 12 characters long';
  }

});

// live feedback
form.username.addEventListener('keyup', e =&gt; {
  // console.log(e.target.value, form.username.value);
  if(usernamePattern.test(e.target.value)){
    // console.log('passed');
    form.username.setAttribute('class', 'success');
  }
  else{
    // console.log('failed');
    form.username.setAttribute('class', 'error');
  }
});
</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3 console.log(e);

const form = document.querySelector('.signup-form');
const feedback = document.querySelector('.feedback');
const usernamePattern = /^&#91;a-zA-Z]{6,12}$/;

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  // validation
  const username = form.username.value;

  if(usernamePattern.test(username)){
    // feedback good info
    feedback.textContent = 'that username is valid!';
  }
  else{
    // feedback help info
    feedback.textContent = 'username must contain letters only &amp; be between 6 &amp; 12 characters long';
  }

});

// live feedback
form.username.addEventListener('keyup', e =&gt; {
  console.log(e);
  // console.log(e.target.value, form.username.value);
  if(usernamePattern.test(e.target.value)){
    // console.log('passed');
    form.username.setAttribute('class', 'success');
  }
  else{
    // console.log('failed');
    form.username.setAttribute('class', 'error');
  }
});
</code></pre>



<h2 class="wp-block-heading">Project – Interactive Ninja Quiz</h2>



<h3 class="wp-block-heading">Project Preview &amp; Setup</h3>



<h3 class="wp-block-heading">Bootstrap Basics</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://getbootstrap.com/" target="_blank">Bootstrap Website</a>&nbsp;– 使用 v4.3</li></ul>



<h3 class="wp-block-heading">HTML Template</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://github.com/iamshaunjp/modern-javascript/tree/lesson-66" target="_blank" rel="noreferrer noopener">GitHub files for this lesson (HTML template)</a></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"&gt;
    &lt;title&gt;Ninja Quiz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;!-- top section --&gt;
    &lt;div class="intro py-3 bg-white text-center"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="text-primary display-3 my-4"&gt;Ninja Quiz&lt;/h2&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- quiz section --&gt;
    &lt;div class="quiz py-4 bg-primary"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="my-5 text-white"&gt;On with the questions...&lt;/h2&gt;

        &lt;form class="quiz-form text-light"&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;1. How do you give a ninja directions?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;Show them a map&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="B"&gt;
              &lt;label class="form-check-label"&gt;Don't worry, a ninja will find you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;2. If a ninja has 3 apples, then gives one to Mario &amp; one to Yoshi, how many apples does the ninja have left?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;1 apple&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="B"&gt;
              &lt;label class="form-check-label"&gt;3 apples, and two corpses&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;3. How do you know when you've met a ninja?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;You'll recognize the outfit&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="B"&gt;
              &lt;label class="form-check-label"&gt;The grim reaper will tell you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;4. What is a ninja's favorite array method?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;forEach()&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="B"&gt;
              &lt;label class="form-check-label"&gt;Slice()&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="text-center"&gt;
            &lt;input type="submit" class="btn btn-light"&gt;
          &lt;/div&gt;
        &lt;/form&gt;

      &lt;/div&gt;
    &lt;/div&gt;

    &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Checking Answers</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"&gt;
    &lt;title&gt;Ninja Quiz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;!-- top section --&gt;
    &lt;div class="intro py-3 bg-white text-center"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="text-primary display-3 my-4"&gt;Ninja Quiz&lt;/h2&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- quiz section --&gt;
    &lt;div class="quiz py-4 bg-primary"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="my-5 text-white"&gt;On with the questions...&lt;/h2&gt;

        &lt;form class="quiz-form text-light"&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;1. How do you give a ninja directions?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;Show them a map&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="B"&gt;
              &lt;label class="form-check-label"&gt;Don't worry, a ninja will find you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;2. If a ninja has 3 apples, then gives one to Mario &amp; one to Yoshi, how many apples does the ninja have left?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;1 apple&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="B"&gt;
              &lt;label class="form-check-label"&gt;3 apples, and two corpses&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;3. How do you know when you've met a ninja?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;You'll recognize the outfit&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="B"&gt;
              &lt;label class="form-check-label"&gt;The grim reaper will tell you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;4. What is a ninja's favorite array method?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;forEach()&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="B"&gt;
              &lt;label class="form-check-label"&gt;Slice()&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="text-center"&gt;
            &lt;input type="submit" class="btn btn-light"&gt;
          &lt;/div&gt;
        &lt;/form&gt;

      &lt;/div&gt;
    &lt;/div&gt;

    &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const correctAnswers = &#91;'B','B','B','B'];
const form = document.querySelector('.quiz-form');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  let score = 0;
  const userAnswers = &#91;form.q1.value, form.q2.value, form.q3.value, form.q4.value];

  // check answers
  userAnswers.forEach((answer, index) =&gt; {
    if(answer === correctAnswers&#91;index]){
      score += 25;
    }
  });

  console.log(score);

});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Showing the Score</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"&gt;
    &lt;title&gt;Ninja Quiz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;!-- top section --&gt;
    &lt;div class="intro py-3 bg-white text-center"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="text-primary display-3 my-4"&gt;Ninja Quiz&lt;/h2&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- result section --&gt;
    &lt;div class="result py-4 d-none bg-light text-center"&gt;
      &lt;div class="container lead"&gt;
        &lt;p&gt;You are &lt;span class="text-primary display-4 p-3"&gt;0%&lt;/span&gt; ninja&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- quiz section --&gt;
    &lt;div class="quiz py-4 bg-primary"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="my-5 text-white"&gt;On with the questions...&lt;/h2&gt;

        &lt;form class="quiz-form text-light"&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;1. How do you give a ninja directions?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;Show them a map&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="B"&gt;
              &lt;label class="form-check-label"&gt;Don't worry, a ninja will find you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;2. If a ninja has 3 apples, then gives one to Mario &amp; one to Yoshi, how many apples does the ninja have left?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;1 apple&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="B"&gt;
              &lt;label class="form-check-label"&gt;3 apples, and two corpses&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;3. How do you know when you've met a ninja?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;You'll recognize the outfit&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="B"&gt;
              &lt;label class="form-check-label"&gt;The grim reaper will tell you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;4. What is a ninja's favorite array method?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;forEach()&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="B"&gt;
              &lt;label class="form-check-label"&gt;Slice()&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="text-center"&gt;
            &lt;input type="submit" class="btn btn-light"&gt;
          &lt;/div&gt;
        &lt;/form&gt;

      &lt;/div&gt;
    &lt;/div&gt;

    &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const correctAnswers = &#91;'B','B','B','B'];
const form = document.querySelector('.quiz-form');
const result = document.querySelector('.result');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  let score = 0;
  const userAnswers = &#91;form.q1.value, form.q2.value, form.q3.value, form.q4.value];

  // check answers
  userAnswers.forEach((answer, index) =&gt; {
    if(answer === correctAnswers&#91;index]){
      score += 25;
    }
  });

  // show result on page
  result.querySelector('span').textContent = `${score}%`;
  result.classList.remove('d-none');
  

});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">The Window Object (觀念、scrollTo)</h3>



<pre class="wp-block-code"><code>// Google Console
&gt;  window
&lt;  Window&nbsp;{window: Window, self: Window, document: document, name: '', location: Location,&nbsp;…}
&gt;  window.outerWidth
&lt;  1280
&gt;  outerWidth
&lt;  1280
&gt;</code></pre>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"&gt;
    &lt;title&gt;Ninja Quiz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;!-- top section --&gt;
    &lt;div class="intro py-3 bg-white text-center"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="text-primary display-3 my-4"&gt;Ninja Quiz&lt;/h2&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- result section --&gt;
    &lt;div class="result py-4 d-none bg-light text-center"&gt;
      &lt;div class="container lead"&gt;
        &lt;p&gt;You are &lt;span class="text-primary display-4 p-3"&gt;0%&lt;/span&gt; ninja&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- quiz section --&gt;
    &lt;div class="quiz py-4 bg-primary"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="my-5 text-white"&gt;On with the questions...&lt;/h2&gt;

        &lt;form class="quiz-form text-light"&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;1. How do you give a ninja directions?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;Show them a map&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="B"&gt;
              &lt;label class="form-check-label"&gt;Don't worry, a ninja will find you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;2. If a ninja has 3 apples, then gives one to Mario &amp; one to Yoshi, how many apples does the ninja have left?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;1 apple&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="B"&gt;
              &lt;label class="form-check-label"&gt;3 apples, and two corpses&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;3. How do you know when you've met a ninja?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;You'll recognize the outfit&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="B"&gt;
              &lt;label class="form-check-label"&gt;The grim reaper will tell you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;4. What is a ninja's favorite array method?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;forEach()&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="B"&gt;
              &lt;label class="form-check-label"&gt;Slice()&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="text-center"&gt;
            &lt;input type="submit" class="btn btn-light"&gt;
          &lt;/div&gt;
        &lt;/form&gt;

      &lt;/div&gt;
    &lt;/div&gt;

    &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const correctAnswers = &#91;'B','B','B','B'];
const form = document.querySelector('.quiz-form');
const result = document.querySelector('.result');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  let score = 0;
  const userAnswers = &#91;form.q1.value, form.q2.value, form.q3.value, form.q4.value];

  // check answers
  userAnswers.forEach((answer, index) =&gt; {
    if(answer === correctAnswers&#91;index]){
      score += 25;
    }
  });

  // show result on page
  scrollTo(0,0);
  result.querySelector('span').textContent = `${score}%`;
  result.classList.remove('d-none');
  

});

// window object (global object)

// console.log('hello');
// window.console.log('hello');

// console.log(document.querySelector('form'));
// console.log(window.document.querySelector('form'));

// alert('hello');
// window.alert('hello');

// setTimeout(() =&gt; {
//   alert('hello, ninjas');
// }, 3000);</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Intervals &amp; Animating the Score</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"&gt;
    &lt;title&gt;Ninja Quiz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;!-- top section --&gt;
    &lt;div class="intro py-3 bg-white text-center"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="text-primary display-3 my-4"&gt;Ninja Quiz&lt;/h2&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- result section --&gt;
    &lt;div class="result py-4 d-none bg-light text-center"&gt;
      &lt;div class="container lead"&gt;
        &lt;p&gt;You are &lt;span class="text-primary display-4 p-3"&gt;0%&lt;/span&gt; ninja&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- quiz section --&gt;
    &lt;div class="quiz py-4 bg-primary"&gt;
      &lt;div class="container"&gt;
        &lt;h2 class="my-5 text-white"&gt;On with the questions...&lt;/h2&gt;

        &lt;form class="quiz-form text-light"&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;1. How do you give a ninja directions?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;Show them a map&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q1" value="B"&gt;
              &lt;label class="form-check-label"&gt;Don't worry, a ninja will find you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;2. If a ninja has 3 apples, then gives one to Mario &amp; one to Yoshi, how many apples does the ninja have left?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;1 apple&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q2" value="B"&gt;
              &lt;label class="form-check-label"&gt;3 apples, and two corpses&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;3. How do you know when you've met a ninja?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;You'll recognize the outfit&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q3" value="B"&gt;
              &lt;label class="form-check-label"&gt;The grim reaper will tell you&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="my-5"&gt;
            &lt;p class="lead font-weight-normal"&gt;4. What is a ninja's favorite array method?&lt;/p&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="A" checked&gt;
              &lt;label class="form-check-label"&gt;forEach()&lt;/label&gt;
            &lt;/div&gt;
            &lt;div class="form-check my-2 text-white-50"&gt;
              &lt;input type="radio" name="q4" value="B"&gt;
              &lt;label class="form-check-label"&gt;Slice()&lt;/label&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="text-center"&gt;
            &lt;input type="submit" class="btn btn-light"&gt;
          &lt;/div&gt;
        &lt;/form&gt;

      &lt;/div&gt;
    &lt;/div&gt;

    &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// app.js

const correctAnswers = &#91;'B','B','B','B'];
const form = document.querySelector('.quiz-form');
const result = document.querySelector('.result');

form.addEventListener('submit', e =&gt; {
  e.preventDefault();

  let score = 0;
  const userAnswers = &#91;form.q1.value, form.q2.value, form.q3.value, form.q4.value];

  // check answers
  userAnswers.forEach((answer, index) =&gt; {
    if(answer === correctAnswers&#91;index]){
      score += 25;
    }
  });

  // show result on page
  scrollTo(0,0);

  result.classList.remove('d-none');
  
  let output = 0;
  const timer = setInterval(() =&gt; {
    result.querySelector('span').textContent = `${output}%`;
    if(output === score){
      clearInterval(timer);
    }
    else{
      output++;
    }
  }, 10);

});

// setTimeout(() =&gt; {
//   // do something
// }, 3000);

// let i = 0;
// const timer = setInterval(() =&gt; {
//   console.log('hello');
//   i++;
//   if(i === 5){
//     clearInterval(timer);
//   }
// }, 1000);</code></pre>



<h2 class="wp-block-heading">Array Methods(陣列方法)</h2>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Filter Method</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Array Methods&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

const scores = &#91;10, 30, 15, 25, 50, 40, 5];

// const filteredScores = scores.filter((score) =&gt; {
//   return score &gt; 20;
// });

// console.log(filteredScores);

const users = &#91;
  {name: 'shaun', premium: true},
  {name: 'yoshi', premium: false},
  {name: 'mario', premium: false},
  {name: 'chun-li', premium: true}
];

const premiumUsers = users.filter(user =&gt; {
  return user.premium
});

console.log(premiumUsers);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

const scores = &#91;10, 30, 15, 25, 50, 40, 5];

// const filteredScores = scores.filter((score) =&gt; {
//   return score &gt; 20;
// });

// console.log(filteredScores);

const users = &#91;
  {name: 'shaun', premium: true},
  {name: 'yoshi', premium: false},
  {name: 'mario', premium: false},
  {name: 'chun-li', premium: true}
];

const premiumUsers = users.filter(user =&gt; user.premium);

console.log(premiumUsers);</code></pre>



<h3 class="wp-block-heading">Map Method</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Array Methods&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

// map method
const prices = &#91;20, 10, 30, 25, 15, 40, 80, 5];

const salePrices = prices.map((price) =&gt; {
  return price / 2;
});

console.log(salePrices);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2 縮寫

// map method
const prices = &#91;20, 10, 30, 25, 15, 40, 80, 5];

const salePrices = prices.map(price =&gt; price / 2);

console.log(salePrices);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3 與 4 做比較

// map method
const prices = &#91;20, 10, 30, 25, 15, 40, 80, 5];

// const salePrices = prices.map(price =&gt; price / 2);
// console.log(salePrices);

const products = &#91;
  {name: 'gold star', price: 20},
  {name: 'mushroom', price: 40},
  {name: 'green shells', price: 30},
  {name: 'banana skin', price: 10},
  {name: 'red shells', price: 50}
];

const saleProducts = products.map((product) =&gt; {
  if(product.price &gt; 30){
    product.price = product.price / 2;
    return product;
  }
  else {
    return product;
  }
});

console.log(saleProducts, products);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 4 與 3 做比較

// map method
const prices = &#91;20, 10, 30, 25, 15, 40, 80, 5];

// const salePrices = prices.map(price =&gt; price / 2);
// console.log(salePrices);

const products = &#91;
  {name: 'gold star', price: 20},
  {name: 'mushroom', price: 40},
  {name: 'green shells', price: 30},
  {name: 'banana skin', price: 10},
  {name: 'red shells', price: 50}
];

const saleProducts = products.map((product) =&gt; {
  if(product.price &gt; 30){
    return {name: product.name, price: product.price / 2};
  }
  else {
    return product;
  }
});

console.log(saleProducts, products);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 5 縮寫

// map method
const prices = &#91;20, 10, 30, 25, 15, 40, 80, 5];

// const salePrices = prices.map(price =&gt; price / 2);
// console.log(salePrices);

const products = &#91;
  {name: 'gold star', price: 20},
  {name: 'mushroom', price: 40},
  {name: 'green shells', price: 30},
  {name: 'banana skin', price: 10},
  {name: 'red shells', price: 50}
];

const saleProducts = products.map(product =&gt; {
  if(product.price &gt; 30){
    return {name: product.name, price: product.price / 2};
  }
  else {
    return product;
  }
});

console.log(saleProducts, products);</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Reduce Method</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Array Methods&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

// reduce method
const scores = &#91;10, 20, 60, 40, 70, 90, 30];

const result = scores.reduce((acc, curr) =&gt; {
  if(curr &gt; 50){
    acc++;
  }
  return acc;
}, 0);

console.log(result);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 1
   3
&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

// reduce method
// const scores = &#91;10, 20, 60, 40, 70, 90, 30];

// const result = scores.reduce((acc, curr) =&gt; {
//   if(curr &gt; 50){
//     acc++;
//   }
//   return acc;
// }, 0);

// console.log(result);

const scores = &#91;
  {player: 'mario', score: 50},
  {player: 'yoshi', score: 30},
  {player: 'mario', score: 70},
  {player: 'crystal', score: 60},
  {player: 'mario', score: 50},
  {player: 'yoshi', score: 30},
  {player: 'mario', score: 70},
  {player: 'crystal', score: 60},
  {player: 'mario', score: 90},
  {player: 'yoshi', score: 30},
  {player: 'mario', score: 30},
  {player: 'crystal', score: 60},
  {player: 'mario', score: 50},
  {player: 'yoshi', score: 30},
  {player: 'mario', score: 80},
  {player: 'crystal', score: 60}
];

const marioTotal = scores.reduce((acc, curr) =&gt; {
  if(curr.player === 'mario'){
    acc += curr.score;
  }
  return acc;
}, 0);

console.log(marioTotal);
</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   490
&gt;</code></pre>



<h4 class="wp-block-heading">補充 MDN 文件</h4>



<ul class="wp-block-list"><li><a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce" target="_blank" rel="noreferrer noopener">Array.prototype.reduce()</a></li></ul>



<h3 class="wp-block-heading">Find Method</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Array Methods&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

// find method
const scores = &#91;10, 5, 0, 40, 30, 10, 90, 70];

const firstHighScore = scores.find((score) =&gt; {
  return score &gt; 50;
});

console.log(firstHighScore);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2 縮寫

// find method
const scores = &#91;10, 5, 0, 40, 30, 10, 90, 70];

const firstHighScore = scores.find(score =&gt; score &gt; 50);

console.log(firstHighScore);</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Sort Method (排序方法)</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Array Methods&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

// example 1 - sorting strings
const names = &#91;'mario', 'shaun', 'chun-li', 'yoshi', 'luigi'];

// names.sort();
// names.reverse();
// console.log(names);


// example 2 - sorting numbers
const scores = &#91;10, 50, 20, 5, 35, 70, 45];

// scores.sort();
// scores.reverse();
// console.log(scores);



// example 3 - sorting objects
const players = &#91;
  {name: 'mario', score: 20},
  {name: 'luigi', score: 10},
  {name: 'chun-li', score: 50},
  {name: 'yoshi', score: 30},
  {name: 'shaun', score: 70}
];

players.sort((a,b) =&gt; {
  if(a.score &gt; b.score){
    return -1;
  } else if (b.score &gt; a.score){
    return 1;
  } else {
    return 0;
  }
});

players.sort((a,b) =&gt; {
  return b.score - a.score;
});

console.log(players);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2 縮寫

// example 1 - sorting strings
const names = &#91;'mario', 'shaun', 'chun-li', 'yoshi', 'luigi'];

// names.sort();
// names.reverse();
// console.log(names);


// example 2 - sorting numbers
const scores = &#91;10, 50, 20, 5, 35, 70, 45];

// scores.sort();
// scores.reverse();
// console.log(scores);



// example 3 - sorting objects
const players = &#91;
  {name: 'mario', score: 20},
  {name: 'luigi', score: 10},
  {name: 'chun-li', score: 50},
  {name: 'yoshi', score: 30},
  {name: 'shaun', score: 70}
];

players.sort((a,b) =&gt; {
  if(a.score &gt; b.score){
    return -1;
  } else if (b.score &gt; a.score){
    return 1;
  } else {
    return 0;
  }
});

players.sort((a,b) =&gt; b.score - a.score);

console.log(players);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 3

// example 1 - sorting strings
const names = &#91;'mario', 'shaun', 'chun-li', 'yoshi', 'luigi'];

// names.sort();
// names.reverse();
// console.log(names);


// example 2 - sorting numbers
const scores = &#91;10, 50, 20, 5, 35, 70, 45];

// scores.sort();
// scores.reverse();
// console.log(scores);



// example 3 - sorting objects
const players = &#91;
  {name: 'mario', score: 20},
  {name: 'luigi', score: 10},
  {name: 'chun-li', score: 50},
  {name: 'yoshi', score: 30},
  {name: 'shaun', score: 70}
];

// players.sort((a,b) =&gt; {
//   if(a.score &gt; b.score){
//     return -1;
//   } else if (b.score &gt; a.score){
//     return 1;
//   } else {
//     return 0;
//   }
// });

players.sort((a,b) =&gt; b.score - a.score);

console.log(players);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 4

// example 1 - sorting strings
const names = &#91;'mario', 'shaun', 'chun-li', 'yoshi', 'luigi'];

// names.sort();
// names.reverse();
// console.log(names);


// example 2 - sorting numbers
const scores = &#91;10, 50, 20, 5, 35, 70, 45];

// scores.sort();
// scores.reverse();
// console.log(scores);

scores.sort((a,b) =&gt; a - b);
console.log(scores);

// example 3 - sorting objects
const players = &#91;
  {name: 'mario', score: 20},
  {name: 'luigi', score: 10},
  {name: 'chun-li', score: 50},
  {name: 'yoshi', score: 30},
  {name: 'shaun', score: 70}
];

// players.sort((a,b) =&gt; {
//   if(a.score &gt; b.score){
//     return -1;
//   } else if (b.score &gt; a.score){
//     return 1;
//   } else {
//     return 0;
//   }
// });

players.sort((a,b) =&gt; b.score - a.score);

console.log(players);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 4
   (7) &#91;5, 10, 20, 35, 45, 50, 70]
   (5) &#91;{...}, {...}, {...}, {...}, {...}
     0: {name: 'shaun', score: 70}
     1: {name: 'chun-li', score: 50}
     2: {name: 'yoshi', score: 30}
     3: {name: 'mario', score: 20}
     4: {name: 'luigi', score: 10}
     length: 5
     &#91;&#91;Prototype]]: Array(0)
&gt;</code></pre>



<h4 class="has-background wp-block-heading" style="background-color:#ff6663">MDN 文件</h4>



<ul class="wp-block-list"><li><a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/sort" target="_blank" rel="noreferrer noopener">Array.prototype.sort()</a></li></ul>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Chaining Array Methods</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Array Methods&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 1

const products = &#91;
  {name: 'gold star', price: 30},
  {name: 'green shell', price: 10},
  {name: 'red shell', price: 40},
  {name: 'banana skin', price: 5},
  {name: 'mushroom', price: 50}
];

const filtered = products.filter(product =&gt; product.price &gt; 20);

const promos = filtered.map(product =&gt; {
  return `the ${product.name} is ${product.price / 2} pounds`;
});

console.log(promos);</code></pre>



<pre class="wp-block-code"><code>// sandbox.js - 2

const products = &#91;
  {name: 'gold star', price: 30},
  {name: 'green shell', price: 10},
  {name: 'red shell', price: 40},
  {name: 'banana skin', price: 5},
  {name: 'mushroom', price: 50}
];

// const filtered = products.filter(product =&gt; product.price &gt; 20);

// const promos = filtered.map(product =&gt; {
//   return `the ${product.name} is ${product.price / 2} pounds`;
// });

const promos = products
  .filter(product =&gt; product.price &gt; 20)
  .map(product =&gt; `the ${product.name} is ${product.price / 2} pounds`);

console.log(promos);</code></pre>



<pre class="wp-block-code"><code>// Google Console - 2
   (3) &#91;'the gold star is 15 pounds', 'the red shell is 20 pounds', 'the mushroom is 25 pounds']
&gt;</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Project – Todo List (待辦清單)</h2>



<h3 class="wp-block-heading">Project Preview and Setup</h3>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>新增專案資料夾 todos</li><li>新增 style.css、app.js 檔案</li><li>新增 index.html 檔案、載入 CSS、JS、Bootstrap、Font Awesome</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Todos&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">HTML &amp; CSS Template</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://github.com/iamshaunjp/modern-javascript/tree/lesson-78" target="_blank">GitHub files for this lesson (HTML template)</a></li></ul>



<h4 class="wp-block-heading">操作步驟</h4>



<ul class="wp-block-list"><li>index.html 程式碼撰寫</li><li>style.css 程式碼撰寫</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Todos&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;div class="container"&gt;

    &lt;header class="text-center text-light my-4"&gt;
      &lt;h1 class="mb-4"&gt;Todo List&lt;/h1&gt;
      &lt;form class="search"&gt;
        &lt;input class="form-control m-auto" type="text" name="search" placeholder="search todos"&gt;
      &lt;/form&gt;
    &lt;/header&gt;

    &lt;ul class="list-group todos mx-auto text-light"&gt;
      &lt;li class="list-group-item d-flex justify-content-between align-items-center"&gt;
        &lt;span&gt;play mariokart&lt;/span&gt;
        &lt;i class="far fa-trash-alt delete"&gt;&lt;/i&gt;
      &lt;/li&gt;
      &lt;li class="list-group-item d-flex justify-content-between align-items-center"&gt;
        &lt;span&gt;defeat ganon in zelda&lt;/span&gt;
        &lt;i class="far fa-trash-alt delete"&gt;&lt;/i&gt;
      &lt;/li&gt;
      &lt;li class="list-group-item d-flex justify-content-between align-items-center"&gt;
        &lt;span&gt;make a veggie pie&lt;/span&gt;
        &lt;i class="far fa-trash-alt delete"&gt;&lt;/i&gt;
      &lt;/li&gt;
    &lt;/ul&gt;

    &lt;form class="add text-center my-4"&gt;
      &lt;label class="text-light"&gt;Add a new todo...&lt;/label&gt;
      &lt;input class="form-control m-auto" type="text" name="add"&gt;
    &lt;/form&gt;

  &lt;/div&gt;

  &lt;script src="app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

body{
  background: #352f5b;
}
.container{
  max-width: 400px;
}
input&#91;type=text],
input&#91;type=text]:focus{
  color: #fff;
  border: none;
  background: rgba(0,0,0,0.2);
  max-width: 400px;
}
.todos li{
  background: #423a6f;
}
.delete{
  cursor: pointer;
}</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Adding Todos</h3>



<pre class="wp-block-code"><code>// app.js

const addForm = document.querySelector('.add');
const list = document.querySelector('.todos');

const generateTemplate = todo =&gt; {

  const html = `
    &lt;li class="list-group-item d-flex justify-content-between align-items-center"&gt;
      &lt;span&gt;${todo}&lt;/span&gt;
      &lt;i class="far fa-trash-alt delete"&gt;&lt;/i&gt;
    &lt;/li&gt;
  `;

  list.innerHTML += html;

}

addForm.addEventListener('submit', e =&gt; {

  e.preventDefault();
  const todo = addForm.add.value.trim();
  // console.log(todo);

  if(todo.length){
    generateTemplate(todo);
    addForm.reset();
  }

});</code></pre>



<h4 class="wp-block-heading">Todo text overflowing outside the list</h4>



<p>解決方式有兩種</p>



<ul class="wp-block-list"><li>word-wrap: break-word;</li><li>word-break: break-all;</li></ul>



<pre class="wp-block-code"><code>// style.css - word-break: break-all;

body{
  background: #352f5b;
}
.container{
  max-width: 400px;
}
input&#91;type=text],
input&#91;type=text]:focus{
  color: #fff;
  border: none;
  background: rgba(0,0,0,0.2);
  max-width: 400px;
}
.todos li{
  background: #423a6f;
}
.delete{
  cursor: pointer;
}

/* todo text overflowing outside the list */
.todos li span{
  word-break: break-all;
}</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Deleting Todos</h3>



<pre class="wp-block-code"><code>// app.js

const addForm = document.querySelector('.add');
const list = document.querySelector('.todos');

const generateTemplate = todo =&gt; {

  const html = `
    &lt;li class="list-group-item d-flex justify-content-between align-items-center"&gt;
      &lt;span&gt;${todo}&lt;/span&gt;
      &lt;i class="far fa-trash-alt delete"&gt;&lt;/i&gt;
    &lt;/li&gt;
  `;

  list.innerHTML += html;

}

addForm.addEventListener('submit', e =&gt; {

  e.preventDefault();
  const todo = addForm.add.value.trim();
  // console.log(todo);

  if(todo.length){
    generateTemplate(todo);
    addForm.reset();
  }

});

// delete todos
list.addEventListener('click', e =&gt; {

  if(e.target.classList.contains('delete')){
    e.target.parentElement.remove();
  }

});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Searching &amp; Filtering Todos</h3>



<p>需重複觀看、練習。</p>



<pre class="wp-block-code"><code>// app.js

const addForm = document.querySelector('.add');
const list = document.querySelector('.todos');
const search = document.querySelector('.search input');

const generateTemplate = todo =&gt; {
  const html = `
    &lt;li class="list-group-item d-flex justify-content-between align-items-center"&gt;
      &lt;span&gt;${todo}&lt;/span&gt;
      &lt;i class="far fa-trash-alt delete"&gt;&lt;/i&gt;
    &lt;/li&gt;
  `;
  list.innerHTML += html;

}

// add todos
addForm.addEventListener('submit', e =&gt; {
  e.preventDefault();
  const todo = addForm.add.value.trim();

  if(todo.length){
    generateTemplate(todo);
    addForm.reset();
  }
});

// delete todos
list.addEventListener('click', e =&gt; {
  if(e.target.classList.contains('delete')){
    e.target.parentElement.remove();
  }
});

const filterTodos = (term) =&gt; {
  Array.from(list.children)
    .filter((todo) =&gt; !todo.textContent.toLowerCase().includes(term))
    .forEach((todo) =&gt; todo.classList.add('filtered'));

  Array.from(list.children)
    .filter((todo) =&gt; todo.textContent.toLowerCase().includes(term))
    .forEach((todo) =&gt; todo.classList.remove('filtered'));
};

// keyup event
search.addEventListener('keyup', () =&gt; {
  const term = search.value.trim().toLowerCase();
  filterTodos(term);
});</code></pre>



<pre class="wp-block-code"><code>// style.css

body{
  background: #352f5b;
}
.container{
  max-width: 400px;
}
input&#91;type=text],
input&#91;type=text]:focus{
  color: #fff;
  border: none;
  background: rgba(0,0,0,0.2);
  max-width: 400px;
}
.todos li{
  background: #423a6f;
}
.delete{
  cursor: pointer;
}
.filtered{
  display: none !important;
}

/* todo text overflowing outside the list */
.todos li span{
  word-break: break-all;
}</code></pre>



<h2 class="wp-block-heading">Dates &amp; Times</h2>



<h3 class="wp-block-heading">Dates &amp; Times in JavaScript</h3>



<h4 class="wp-block-heading">JavaScript Data Types</h4>



<figure class="wp-block-table"><table><tbody><tr><td>Object</td><td>Arrays, Object Literals, Functions,&nbsp;<strong>Dates</strong>&nbsp;etc</td></tr></tbody></table></figure>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Dates &amp; Times&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

// dates &amp; times
const now = new Date();

console.log(now);
console.log(typeof now);

// year, months, day, times
console.log('getFullYear:', now.getFullYear());
console.log('getMonth:', now.getMonth());
console.log('getDate:', now.getDate());
console.log('getDay:', now.getDay());
console.log('getHours:', now.getHours());
console.log('getMinutes:', now.getMinutes());
console.log('getSeconds:', now.getSeconds());

// timestamps
console.log('timestamp:', now.getTime());

// date strings
console.log(now.toDateString());
console.log(now.toTimeString());
console.log(now.toLocaleString());</code></pre>



<h3 class="wp-block-heading">Timestamps &amp; Comparisons (時間戳記 &amp; 比較)</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h1&gt;Dates &amp; Times&lt;/h1&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

// timestamps

const before = new Date('February 1 2019 7:30:59');
const now = new Date();

// console.log(now.getTime(), before.getTime());

const diff = now.getTime() - before.getTime();
console.log(diff);

const mins = Math.round(diff / 1000 / 60);
const hours = Math.round(mins / 60);
const days = Math.round(hours / 24);

console.log(mins, hours, days);

console.log(`the blog was written ${days} days ago`);

// converting timestamps into date objects
const timestamp = 1675938474990;
console.log(new Date(timestamp));</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Building a Digital Clock</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
    &lt;style&gt;
        body{
            background: #333;
        }
        .clock{
            font-size: 4em;
            text-align: center;
            margin: 200px auto;
            color: yellow;
            font-family: arial;
        }
        .clock span{
            padding: 20px;
            background: #444;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;div class="clock"&gt;&lt;/div&gt;
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const clock = document.querySelector('.clock');

const tick = () =&gt; {

  const now = new Date();

  const h = now.getHours();
  const m = now.getMinutes();
  const s = now.getSeconds();

  // console.log(h, m, s);
  const html = `
    &lt;span&gt;${h}&lt;/span&gt; :
    &lt;span&gt;${m}&lt;/span&gt; :
    &lt;span&gt;${s}&lt;/span&gt;
  `;

  clock.innerHTML = html;

};

setInterval(tick, 1000);</code></pre>



<h3 class="wp-block-heading">Date-fns Library</h3>



<h4 class="wp-block-heading">資源</h4>



<ul class="wp-block-list"><li><a href="https://date-fns.org/" target="_blank" rel="noreferrer noopener">Date-fns Library Website</a></li></ul>



<h4 class="wp-block-heading">Date-fns Library 文件使用</h4>



<ul class="wp-block-list"><li>isAfter</li><li>isToday</li><li>format</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;div class="clock"&gt;&lt;/div&gt;
    
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.9.0/date_fns.min.js"&gt;&lt;/script&gt;
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

const now = new Date();

// console.log(dateFns.isToday(now));

// formatting options
console.log(dateFns.format(now, 'YYYY'));
console.log(dateFns.format(now, 'MMMM'));
console.log(dateFns.format(now, 'MMM'));
console.log(dateFns.format(now, 'dddd'));
console.log(dateFns.format(now, 'Do'));
console.log(dateFns.format(now, 'dddd, Do, MMMM, YYYY'));
console.log(dateFns.format(now, 'dddd Do MMMM YYYY'));

// comparing dates
const before = new Date('February 1 2019 12:00:00');

console.log(dateFns.distanceInWords(now, before, {addSuffix: true}));</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">Async JavaScript (非同步)</h2>



<p>這個章節重要、比較困難，需要重複觀看、練習。</p>



<h3 class="wp-block-heading">What is Asynchronous JavaScript?</h3>



<h4 class="wp-block-heading">Async JavaScript</h4>



<ul class="wp-block-list"><li>Governs how we perform tasks which take some time to complete<br>(e.g. Getting data from a database)</li></ul>



<p>Start something now and finish it later</p>



<h4 class="wp-block-heading">Synchronous JavaScript (同步)</h4>



<ul class="wp-block-list"><li>JavaScript can run ONE statement at a time</li></ul>



<pre class="wp-block-code"><code>console.log('line one');
console.log('line two');
console.log('line three');</code></pre>



<h4 class="wp-block-heading">Single Threaded</h4>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1183" height="757" src="/wordpress_blog/wp-content/uploads/2022/04/single-threaded.png" alt="" class="wp-image-493"/><figcaption>Single Threaded</figcaption></figure>



<h4 class="wp-block-heading">Async to the Rescue…</h4>



<p>Start something now &amp; finish it later</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1283" height="829" src="/wordpress_blog/wp-content/uploads/2022/04/async-to-the-rescue.png" alt="" class="wp-image-494"/><figcaption>Async to the rescue</figcaption></figure>



<h3 class="wp-block-heading">Async Code in Action</h3>



<pre class="wp-block-code"><code>// index.html

&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;Modern JavaScript&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

    
    
    &lt;script src="sandbox.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sandbox.js

console.log(1);
console.log(2);

setTimeout(() =&gt; {
  console.log('callback function fired');
}, 2000);

console.log(3);
console.log(4);</code></pre>



<pre class="wp-block-code"><code>// Google Console
   1
   2
   3
   4
   callback function fired
&gt;</code></pre>



<h3 class="wp-block-heading">What are HTTP Requests?</h3>



<h4 class="wp-block-heading">HTTP Requests</h4>



<ul class="wp-block-list"><li>Make HTTP requests to get data from another server</li><li>We make these requests to API endpoints</li></ul>



<h4 class="wp-block-heading">API Endpoints</h4>



<ul class="wp-block-list"><li>Example API endpoint:<br>http://www.musicapi.com/artist/moby</li></ul>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1536" height="465" src="/wordpress_blog/wp-content/uploads/2022/04/api-endpoints.png" alt="" class="wp-image-496"/><figcaption>API Endpoints</figcaption></figure>



<h4 class="wp-block-heading">JSONPlaceholder</h4>



<ul class="wp-block-list"><li><a href="https://jsonplaceholder.typicode.com/" target="_blank" rel="noreferrer noopener">JSONPlaceholder 連結</a></li></ul>



<h4 class="wp-block-heading">Google Network</h4>



<ul class="wp-block-list"><li>Name – 1</li><li>Headers</li><li>Response</li></ul>



<h3 class="wp-block-heading">Making HTTP Requests (XHR)</h3>



<pre class="wp-block-code"><code>// sandbox.js

const request = new XMLHttpRequest();

request.addEventListener('readystatechange', () =&gt; {
  // console.log(request, request.readyState);
  if(request.readyState === 4){
    console.log(request.responseText);
  }
});

request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
request.send();</code></pre>



<h4 class="wp-block-heading">MDN Web Docs</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://developer.mozilla.org/zh-TW/docs/Web/API/XMLHttpRequest/readyState" target="_blank">XMLHttpRequest.readyState MDN 文件</a></li><li><a href="https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status" target="_blank" rel="noreferrer noopener">HTTP 狀態碼</a></li></ul>



<h3 class="wp-block-heading">Response Status</h3>



<ul class="wp-block-list"><li>404</li><li>200</li></ul>



<pre class="wp-block-code"><code>// sandbox.js

const request = new XMLHttpRequest();

request.addEventListener('readystatechange', () =&gt; {
  // console.log(request, request.readyState);
  if(request.readyState === 4 &amp;&amp; request.status === 200){
    console.log(request, request.responseText);
  } else if(request.readyState === 4){
    console.log('could not fetch the data');
  }
});

request.open('GET', 'https://jsonplaceholder.typicode.com/todoss/');
request.send();</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Callback Functions (回呼函式)</h3>



<pre class="wp-block-code"><code>// sandbox.js

const getTodos = (callback) =&gt; {
  const request = new XMLHttpRequest();

  request.addEventListener('readystatechange', () =&gt; {
    // console.log(request, request.readyState);
    if(request.readyState === 4 &amp;&amp; request.status === 200){
      // console.log(request, request.responseText);
      callback(undefined, request.responseText);
    } else if(request.readyState === 4){
      // console.log('could not fetch the data');
      callback('could not fetch data', undefined);
    }
  });

  request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
  request.send();
};

console.log(1);
console.log(2);

getTodos((err, data) =&gt; {
  console.log('callback fired');
  // console.log(err, data);
  if(err){
    console.log(err);
  } else {
    console.log(data);
  }
});

console.log(3);
console.log(4);</code></pre>



<h3 class="wp-block-heading">JSON Data</h3>



<pre class="wp-block-code"><code>// sandbox.js

const getTodos = (callback) =&gt; {
  const request = new XMLHttpRequest();

  request.addEventListener('readystatechange', () =&gt; {
    if(request.readyState === 4 &amp;&amp; request.status === 200){
      const data = JSON.parse(request.responseText);
      callback(undefined, data);
    } else if(request.readyState === 4){
      callback('could not fetch data', undefined);
    }
  });

  request.open('GET', 'todos.json');
  request.send();
};

getTodos((err, data) =&gt; {
  console.log('callback fired');
  if(err){
    console.log(err);
  } else {
    console.log(data);
  }
});
</code></pre>



<pre class="wp-block-code"><code>// todos.json

&#91;
  { "text": "play mariokart", "author": "Shaun" },
  { "text": "buy some bread", "author": "Mario" },
  { "text": "do the plumming", "author": "Luigi" }
]</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Callback Hell</h3>



<pre class="wp-block-code"><code>// sandbox.js

const getTodos = (resource, callback) =&gt; {
  const request = new XMLHttpRequest();

  request.addEventListener('readystatechange', () =&gt; {
    if(request.readyState === 4 &amp;&amp; request.status === 200){
      const data = JSON.parse(request.responseText);
      callback(undefined, data);
    } else if(request.readyState === 4){
      callback('could not fetch data', undefined);
    }
  });

  request.open('GET', resource);
  request.send();
};

getTodos('todos/luigi.json', (err, data) =&gt; {
  console.log(data);
  getTodos('todos/mario.json', (err, data) =&gt; {
    console.log(data);
    getTodos('todos/shaun.json', (err, data) =&gt; {
      console.log(data);
    })
  })
});
</code></pre>



<pre class="wp-block-code"><code>// todos/luigi.json

&#91;
  { "text": "do the plumming", "author": "Luigi" },
  { "text": "avoid mario", "author": "Luigi" },
  { "text": "go kart racing", "author": "Luigi" }
]</code></pre>



<pre class="wp-block-code"><code>// todos/mario.json

&#91;
  { "text": "make fun of luigi", "author": "Mario" },
  { "text": "rescue peach (again)", "author": "Mario" },
  { "text": "go kart racing", "author": "Mario" }
]</code></pre>



<pre class="wp-block-code"><code>// todos/shaun.json

&#91;
  { "text": "play mariokart", "author": "Shaun" },
  { "text": "buy some bread", "author": "Shaun" },
  { "text": "take a nap", "author": "Shaun" }
]</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Promise Basics</h3>



<pre class="wp-block-code"><code>// sandbox.js

const getTodos = (resource) =&gt; {
  
  return new Promise((resolve, reject) =&gt; {
    const request = new XMLHttpRequest();

    request.addEventListener('readystatechange', () =&gt; {
      if(request.readyState === 4 &amp;&amp; request.status === 200){
        const data = JSON.parse(request.responseText);
        resolve(data);
      } else if(request.readyState === 4){
        reject('error getting resource');
      }
    });

    request.open('GET', resource);
    request.send();
  });

};

getTodos('todos/luigi.json').then(data =&gt; {
  console.log('promise resolved:', data);
}).catch(err =&gt; {
  console.log('promise rejected:', err);
});

// promise example

// const getSomething = () =&gt; {

//   return new Promise((resolve, reject) =&gt; {
//     // fetch something
//     resolve('some data');
//     // reject('some error');
//   });

// };

// getSomething().then((data) =&gt; {
//   console.log(data);
// }, (err) =&gt; {
//   console.log(err);
// });

// getSomething().then(data =&gt; {
//   console.log(data);
// }).catch(err =&gt; {
//   console.log(err);
// });</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Chaining Promises</h3>



<pre class="wp-block-code"><code>// sandbox.js

const getTodos = (resource) =&gt; {
  
  return new Promise((resolve, reject) =&gt; {
    const request = new XMLHttpRequest();

    request.addEventListener('readystatechange', () =&gt; {
      if(request.readyState === 4 &amp;&amp; request.status === 200){
        const data = JSON.parse(request.responseText);
        resolve(data);
      } else if(request.readyState === 4){
        reject('error getting resource');
      }
    });

    request.open('GET', resource);
    request.send();
  });

};

getTodos('todos/luigi.json').then(data =&gt; {
  console.log('promise 1 resolved:', data);
  return getTodos('todos/mario.json');
}).then(data =&gt; {
  console.log('promise 2 resolved:', data);
  return getTodos('todos/shauns.json');
}).then(data =&gt; {
  console.log('promise 3 resolved:', data);
}).catch(err =&gt; {
  console.log('promise rejected:', err);
});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">The Fetch API</h3>



<pre class="wp-block-code"><code>// sandbox.js

// fetch api

fetch('todos/luigi.json').then((response) =&gt; {
  console.log('resolved', response);
  return response.json();
}).then(data =&gt; {
  console.log(data);
}).catch((err) =&gt; {
  console.log('rejected', err);
});</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Async &amp; Await</h3>



<pre class="wp-block-code"><code>// sandbox.js

// async &amp; await

const getTodos = async () =&gt; {

  const response = await fetch('todos/luigi.json');
  const data = await response.json();
  
  return data;

};

console.log(1);
console.log(2);

getTodos()
  .then(data =&gt; console.log('resolved:', data));

console.log(3);
console.log(4);

// fetch('todos/luigi.json').then((response) =&gt; {
//   console.log('resolved', response);
//   return response.json();
// }).then(data =&gt; {
//   console.log(data);
// }).catch((err) =&gt; {
//   console.log('rejected', err);
// });</code></pre>



<h3 class="has-background wp-block-heading" style="background-color:#ff6663">Throwing &amp; Catching Errors</h3>



<pre class="wp-block-code"><code>// sandbox.js

// async &amp; await

const getTodos = async () =&gt; {

  const response = await fetch('todos/luigis.json');

  if(response.status !== 200){
    throw new Error('cannot fetch the data');
  }

  const data = await response.json();
  
  return data;

};

getTodos()
  .then(data =&gt; console.log('resolved:', data))
  .catch(err =&gt; console.log('rejected:', err.message));</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>CSS Grid Tutorial</title>
		<link>/wordpress_blog/css-grid-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Sun, 16 Jan 2022 02:59:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=484</guid>

					<description><![CDATA[#1 – Why Use CSS Grid? In this f [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">#1 – Why Use CSS Grid?</h2>



<p>In this first CSS grid tutorial, I’ll show you why CSS grid is so awesome, and compare it to other layout techniques such as floats and flexbox.</p>



<h3 class="wp-block-heading">COURSE LINKS</h3>



<ul class="wp-block-list"><li><a href="https://atom.io/" target="_blank" rel="noreferrer noopener">Atom editor</a></li><li><a href="https://github.com/iamshaunjp/css-grid-playlist" target="_blank" rel="noreferrer noopener">GitHub Repo</a></li></ul>



<h4 class="wp-block-heading">floats、flex、grid 範例程式碼</h4>



<pre class="wp-block-code"><code>// floats.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using Floats&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
			line-height: 3em;
		}
		#content{
			max-width: 960px;
			margin: 0 auto;
		}
		header{
			background: #3bbced;
			height: 50px;
		}
		main{
			float: right;
			width: 60%;
			background: #e82b69;
			height: 400px;
		}
		aside{
			background: #fac24a;
			height: 200px;
		}
		nav{
			background: #8ae348;
			height: 200px;
		}
		footer{
			background: #a56dda;
			height: 100px;
		}
		#sidebar{
			float: left;
			width: 40%;
		}
		#middle:after{
			content: '';
			display: block;
			clear: both;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;
		
		&lt;header&gt;Header&lt;/header&gt;

		&lt;div id="middle"&gt;&lt;!-- extra markup --&gt;

			&lt;main&gt;Main&lt;/main&gt;

			&lt;div id="sidebar"&gt;&lt;!-- extra markup --&gt;

				&lt;aside&gt;Aside&lt;/aside&gt;

				&lt;nav&gt;Nav&lt;/nav&gt;

			&lt;/div&gt;

		&lt;/div&gt;

		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// flex.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using Flexbox&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
			line-height: 3em;
		}
		#content{
			max-width: 960px;
			margin: 0 auto;
		}
		#middle{
			display: flex;
		}
		#sidebar{
			flex-grow: 1;
		}
		header{
			background: #3bbced;
			height: 50px;
		}
		main{
			background: #e82b69;
			flex-grow: 2;
		}
		aside{
			background: #fac24a;
			flex-grow: 1;
			height: 200px;
		}
		nav{
			background: #8ae348;
			flex-grow: 1;
			height: 200px;
		}
		footer{
			background: #a56dda;
			height: 100px;
		}
		#middle:after{
			content: '';
			display: block;
			clear: both;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;header&gt;Header&lt;/header&gt;

		&lt;div id="middle"&gt;&lt;!-- extra markup --&gt;

			&lt;main&gt;Main&lt;/main&gt;

			&lt;div id="sidebar"&gt;&lt;!-- extra markup --&gt;

				&lt;aside&gt;Aside&lt;/aside&gt;

				&lt;nav&gt;Nav&lt;/nav&gt;

			&lt;/div&gt;

		&lt;/div&gt;

		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
</code></pre>



<pre class="wp-block-code"><code>// grids.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: 1fr 2fr;
			grid-auto-rows: minmax(150px, auto);
			max-width: 960px;
			margin: 0 auto;
		}
		#content &gt; *{
			padding: 10px;
		}
		header{
			grid-column: span 3;
			background: #3bbced;
		}
		main{
			grid-column: 2 / 4;
			grid-row: 2 / 4;
			background: #e82b69;
		}
		aside{
			grid-column: 1 / 2;
			background: #fac24a;
		}
		nav{
			grid-column: 1 / 2;
			background: #8ae348;
		}
		footer{
			grid-column: span 3;
			background: #a56dda;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;header&gt;Header&lt;/header&gt;

		&lt;main&gt;Main&lt;/main&gt;

		&lt;aside&gt;Aside&lt;/aside&gt;

		&lt;nav&gt;Nav&lt;/nav&gt;

		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h4 class="wp-block-heading">CSS Grid</h4>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-7 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1536" height="731" data-id="485" src="/wordpress_blog/wp-content/uploads/2022/04/grid-desktop-mobile.jpg" alt="" class="wp-image-485"/><figcaption>Grid Desktop Mobile</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1102" height="1035" data-id="486" src="/wordpress_blog/wp-content/uploads/2022/04/grid-webpage.jpg" alt="" class="wp-image-486"/><figcaption>Grid Webpage</figcaption></figure>
</figure>



<h2 class="wp-block-heading">#2 – Columns</h2>



<p>In this CSS grid tutorial, I’ll show you how to work with grid columns – the amount of columns you want in your grid, the width of those columns, and how content is placed onto them.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			/* grid-template-columns: 33.3% 33.3% 33.3%; */
			/* grid-template-columns: 30% 20% 50%; */
			/* grid-template-columns: 1fr 1fr 1fr; */
			/* grid-template-columns: 1fr 2fr 1fr; */
			/* grid-template-columns: repeat(3, 1fr); */
			grid-template-columns: repeat(9, 1fr);
			max-width: 960px;
			margin: 0 auto;
		}
		#content div{
			background: #3bbced;
			padding: 30px;
		}
		#content div:nth-child(even){
			background: #777;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div&gt;1&lt;/div&gt;
		&lt;div&gt;2&lt;/div&gt;
		&lt;div&gt;3&lt;/div&gt;
		&lt;div&gt;4&lt;/div&gt;
		&lt;div&gt;5&lt;/div&gt;
		&lt;div&gt;6&lt;/div&gt;
		&lt;div&gt;7&lt;/div&gt;
		&lt;div&gt;8&lt;/div&gt;
		&lt;div&gt;9&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h4 class="wp-block-heading">格線布局的基本概念</h4>



<ul class="wp-block-list"><li><a href="https://developer.mozilla.org/zh-TW/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout" target="_blank" rel="noreferrer noopener">MDN Web Docs 連結</a></li></ul>



<h2 class="wp-block-heading">#3 – Rows</h2>



<p>In this CSS grid tutorial I’ll talk about grid rows.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(3, 1fr);
			/* grid-auto-rows: 200px; */
			/* grid-auto-rows: minmax(200px, auto); */
			/* grid-template-rows: 200px 300px 400px 200px; */
			/* grid-template-rows: repeat(3, 200px); */
			/* grid-template-rows: repeat(3, minmax(200px, auto)); */
			grid-template-rows: repeat(10, minmax(200px, auto));
			/* grid-column-gap: 10px; */
			/* column-gap: 10px; */
			/* grid-row-gap: 10px; */
			/* row-gap: 10px; */
			/* grid-gap: 10px; */
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
		}
		#content div{
			background: #3bbced;
			padding: 30px;
		}
		#content div:nth-child(even){
			background: #777;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div&gt;1&lt;/div&gt;
		&lt;div&gt;2&lt;/div&gt;
		&lt;div&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Praesentium voluptates suscipit temporibus magnam natus et architecto eligendi animi id dolores! Minima, commodi iure voluptatum quibusdam consequuntur dolores tempora sapiente dignissimos repudiandae nam illo rem blanditiis non aspernatur architecto odio, veniam facilis at. Soluta doloribus rem odit sit voluptatem nobis maxime qui eos doloremque? Sequi dolore amet consequuntur iste excepturi reprehenderit.&lt;/div&gt;
		&lt;div&gt;4&lt;/div&gt;
		&lt;div&gt;5&lt;/div&gt;
		&lt;div&gt;6&lt;/div&gt;
		&lt;div&gt;7&lt;/div&gt;
		&lt;div&gt;8&lt;/div&gt;
		&lt;div&gt;9&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h4 class="wp-block-heading">屬性更新說明</h4>



<p>grid-column-gap, grid-row-gap and grid-gap are obsolete and replaced with column-gap, row-gap and gap</p>



<h2 class="wp-block-heading">#4 – Grid Lines</h2>



<p>In this CSS grid tutorial I’ll show you how to use CSS grid lines to position elements on a grid wherever you want within the grid, regardless of it’s position inside your HTML markup.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1123" height="959" src="/wordpress_blog/wp-content/uploads/2022/04/grid-lines.jpg" alt="" class="wp-image-488"/><figcaption>Grid Lines</figcaption></figure>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(6, 1fr);
			grid-template-rows: repeat(4, minmax(150px, auto));
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
		}
		#content div{
			background: #3bbced;
			padding: 30px;
		}
		#content div:nth-child(even){
			background: #777;
			padding: 30px;
		}
		.one{
			/* grid-column-start: 1;
			grid-column-end: 3; */
			grid-column: 1 / 3;
		}
		.two{
			grid-column: 3 / 7;
		}
		.three{
			grid-column: 1 / 4;
			grid-row: 2 / 4;
		}
		.four{
			grid-column: 4 / 7;
			grid-row: 2 / 4;
		}
		.five{
			grid-column: 3 / 7;
		}
		.six{
			grid-column: 1 / 3;
			grid-row: 4;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div class="one"&gt;1&lt;/div&gt;
		&lt;div class="two"&gt;2&lt;/div&gt;
		&lt;div class="three"&gt;3&lt;/div&gt;
		&lt;div class="four"&gt;4&lt;/div&gt;
		&lt;div class="five"&gt;5&lt;/div&gt;
		&lt;div class="six"&gt;6&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="wp-block-heading">#5 – Nested Grids</h2>



<p>In this CSS grid tutorial I’ll show you how we can nest grids within each other. Very simply.</p>



<pre class="wp-block-code"><code>// index.html - 1

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(3, 1fr);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
		}
		#content div{
			background: #3bbced;
			padding: 30px;
		}
		#content div:nth-child(even){
			background: #777;
		}
		.nested{
			display: grid;
			grid-template-columns: 1fr 1fr;
			gap: 10px;
		}
		.nested p{
			border: 1px solid #fff;
			padding: 20px;
			margin: 0;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div&gt;1&lt;/div&gt;
		&lt;div&gt;2&lt;/div&gt;
		&lt;div&gt;3&lt;/div&gt;
		&lt;div class="nested"&gt;
			&lt;p&gt;1&lt;/p&gt;
			&lt;p&gt;2&lt;/p&gt;
			&lt;p&gt;3&lt;/p&gt;
			&lt;p&gt;4&lt;/p&gt;
		&lt;/div&gt;
		&lt;div&gt;5&lt;/div&gt;
		&lt;div&gt;6&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// index.html - 2

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(3, 1fr);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
		}
		#content div{
			background: #3bbced;
			padding: 30px;
		}
		#content div:nth-child(even){
			background: #777;
		}
		.nested{
			display: grid;
			grid-template-columns: 1fr 1fr;
			gap: 10px;
			grid-column: span 3;
		}
		.nested p{
			border: 1px solid #fff;
			padding: 20px;
			margin: 0;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div&gt;1&lt;/div&gt;
		&lt;div&gt;2&lt;/div&gt;
		&lt;div&gt;3&lt;/div&gt;
		&lt;div class="nested"&gt;
			&lt;p&gt;1&lt;/p&gt;
			&lt;p&gt;2&lt;/p&gt;
			&lt;p&gt;3&lt;/p&gt;
			&lt;p&gt;4&lt;/p&gt;
		&lt;/div&gt;
		&lt;div&gt;5&lt;/div&gt;
		&lt;div&gt;6&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="wp-block-heading">#6 – Aligning &amp; Justifying Items</h2>



<h4 class="wp-block-heading">align-items</h4>



<ul class="wp-block-list"><li>start</li><li>end</li><li>stretch</li></ul>



<h4 class="wp-block-heading">justify-items</h4>



<ul class="wp-block-list"><li>start</li><li>end</li></ul>



<h4 class="wp-block-heading">justify-self</h4>



<ul class="wp-block-list"><li>start</li><li>end</li><li>center</li></ul>



<h4 class="wp-block-heading">align-self</h4>



<ul class="wp-block-list"><li>start</li><li>end</li><li>center</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(3, 1fr);
			grid-auto-rows: minmax(150px, auto);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
			/* align-items: start;
			justify-items: end; */
		}
		#content div{
			background: #3bbced;
			padding: 30px;
		}
		#content div:nth-child(even){
			background: #777;
		}
		.one{
			justify-self: end;
			align-self: end;
		}
		.two{
			justify-self: center;
			align-self: center;
		}
		.three{
			align-self: start;
			justify-self: start;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div class="one"&gt;1&lt;/div&gt;
		&lt;div class="two"&gt;2&lt;/div&gt;
		&lt;div class="three"&gt;3&lt;/div&gt;
		&lt;div class="four"&gt;4&lt;/div&gt;
		&lt;div class="five"&gt;5&lt;/div&gt;
		&lt;div class="six"&gt;6&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#7 – Create a 12-Column Grid</h2>



<p>In this CSS grid tutorial, I’ll show you how we can create a 12-column grid using the CSS grid properties I’ve shown you so far. I’ll also show you how to create a grid overlay so you can visualize the grid on the page.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(12, 1fr);
			grid-auto-rows: minmax(100px, auto);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
			position: relative;
		}
		#content &gt; *{
			background: #3bbced;
			padding: 30px;
		}
		header{
			grid-column: 1 / 13;
		}
		main{
			grid-column: 4 / 13;
			grid-row: 2 / 4;
		}
		aside{
			grid-column: 1 / 4;
		}
		section{
			grid-column: 1 / 13;
			grid-row: 4 / 6;
		}
		nav{
			grid-column: 1 / 4;
		}
		footer{
			grid-column: 1 / 13;
		}
		#grid{
			display: grid;
			position: absolute;
			top: 0;
			left: 0;
			grid-template-columns: repeat(12, 1fr);
			grid-auto-rows: minmax(100%, auto);
			width: 100%;
			height: 100%;
			background: transparent;
			padding: 0;
			display: none;
		}
		#grid p{
			border: 1px solid;
			background: #000;
			margin: 0;
			opacity: 0.2;
		}
		input:checked + #content #grid{
			display: grid;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;input type="checkbox"&gt;
	&lt;div id="content"&gt;

		&lt;div id="grid"&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
			&lt;p&gt;&lt;/p&gt;
		&lt;/div&gt;

		&lt;header&gt;Header&lt;/header&gt;
		&lt;main&gt;Main&lt;/main&gt;
		&lt;section&gt;Section&lt;/section&gt;
		&lt;aside&gt;Aside&lt;/aside&gt;
		&lt;nav&gt;Nav&lt;/nav&gt;
		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="wp-block-heading">#8 – Mosaic Layout</h2>



<p>In this CSS grid tutorial I’ll show you how to make a mosaic layout using CSS grid techniques we’ve already learnt.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(6, 1fr);
			grid-auto-rows: minmax(150px, auto);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
		}
		#content div{
			background: #333;
			padding: 30px;
		}
		.one{
			grid-column: 1 / 3;
			grid-row: 1 / 5;
		}
		.two{
			grid-column: 3 / 7;
			grid-row: 1 / 3;
		}
		.three{
			grid-column: 3 / 5;
			grid-row: 3 / 5;
		}
		.four{
			grid-column: 5 / 7;
			grid-row: 3 / 7;
		}
		.five{
			grid-column: 1 / 5;
			grid-row: 5 / 7;
		}
		#content{
			transform: rotateZ(45deg)scale(0.7);
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;div class="one"&gt;1&lt;/div&gt;
		&lt;div class="two"&gt;2&lt;/div&gt;
		&lt;div class="three"&gt;3&lt;/div&gt;
		&lt;div class="four"&gt;4&lt;/div&gt;
		&lt;div class="five"&gt;5&lt;/div&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#9 – Grid Areas</h2>



<p>In this CSS grid tutorial I want to show you how we can create a grid-like structure using grid areas and the grid-template-areas property.</p>



<pre class="wp-block-code"><code>// index.html - 1

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(4, 1fr);
			grid-auto-rows: minmax(100px, auto);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
			grid-template-areas:
			"header header header header"
			"aside aside main main"
			"nav nav main main"
			"section section section section"
			"section section section section"
			"footer footer footer footer";
		}
		#content &gt; *{
			background: #3bbced;
			padding: 30px;
		}
		header{
			grid-area: header;
		}
		main{
			grid-area: main;
		}
		section{
			grid-area: section;
		}
		aside{
			grid-area: aside;
		}
		nav{
			grid-area: nav;
		}
		footer{
			grid-area: footer;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;header&gt;Header&lt;/header&gt;
		&lt;main&gt;Main&lt;/main&gt;
		&lt;section&gt;Section&lt;/section&gt;
		&lt;aside&gt;Aside&lt;/aside&gt;
		&lt;nav&gt;Nav&lt;/nav&gt;
		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h4 class="wp-block-heading">使用 . 產生空白區域</h4>



<pre class="wp-block-code"><code>// index.html - 2

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(4, 1fr);
			grid-auto-rows: minmax(100px, auto);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
			grid-template-areas:
			"header header header header"
			"aside . main main"
			"nav . main main"
			"section section section section"
			"section section section section"
			"footer footer footer footer";
		}
		#content &gt; *{
			background: #3bbced;
			padding: 30px;
		}
		header{
			grid-area: header;
		}
		main{
			grid-area: main;
		}
		section{
			grid-area: section;
		}
		aside{
			grid-area: aside;
		}
		nav{
			grid-area: nav;
		}
		footer{
			grid-area: footer;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;header&gt;Header&lt;/header&gt;
		&lt;main&gt;Main&lt;/main&gt;
		&lt;section&gt;Section&lt;/section&gt;
		&lt;aside&gt;Aside&lt;/aside&gt;
		&lt;nav&gt;Nav&lt;/nav&gt;
		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#10 – Responsive Grid Example</h2>



<p>In this CSS grid tutorial I’ll demonstrate how we can use media queries and grid areas to make a full responsive CSS grid.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
	&lt;title&gt;Using CSS Grid&lt;/title&gt;
	&lt;style&gt;
		body{
			color: #fff;
			font-family: 'Nunito Semibold';
			text-align: center;
		}
		#content{
			display: grid;
			grid-template-columns: repeat(4, 1fr);
			grid-auto-rows: minmax(100px, auto);
			gap: 10px;
			max-width: 960px;
			margin: 0 auto;
			grid-template-areas:
			"header header header header"
			"aside aside main main"
			"nav nav main main"
			"section section section section"
			"section section section section"
			"footer footer footer footer";
		}
		/* desktop grid */
		@media screen and (min-width: 760px){
			#content{
				display: grid;
				grid-template-columns: repeat(4, 1fr);
				grid-auto-rows: minmax(100px, auto);
				gap: 10px;
				max-width: 960px;
				margin: 0 auto;
				grid-template-areas:
				"header header header header"
				"footer footer footer footer"
				"main main main main"
				"main main main main"
				"aside aside nav nav"
				"section section section section"
				"section section section section";
			}
		}
		#content &gt; *{
			background: #3bbced;
			padding: 30px;
		}
		header{
			grid-area: header;
		}
		main{
			grid-area: main;
		}
		section{
			grid-area: section;
		}
		aside{
			grid-area: aside;
		}
		nav{
			grid-area: nav;
		}
		footer{
			grid-area: footer;
		}
	&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

	&lt;div id="content"&gt;

		&lt;header&gt;Header&lt;/header&gt;
		&lt;main&gt;Main&lt;/main&gt;
		&lt;section&gt;Section&lt;/section&gt;
		&lt;aside&gt;Aside&lt;/aside&gt;
		&lt;nav&gt;Nav&lt;/nav&gt;
		&lt;footer&gt;Footer&lt;/footer&gt;

	&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>HTML &#038; CSS Crash Course Tutorial</title>
		<link>/wordpress_blog/htmlcss-crash/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Sun, 09 Jan 2022 01:36:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=472</guid>

					<description><![CDATA[#01 – Introduction Hey gang &#038;amp [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">#01 – Introduction</h2>



<p>Hey gang &amp; welcome to your very first HTML &amp; CSS tutorial. Throughout this crash course series I’ll take you from total beginner to create great-looking sites with HTML &amp; CSS. In this video, we’ll cover what HTML &amp; CSS are, as well as setting up our dev environment.</p>



<h3 class="wp-block-heading">HTML &amp; CSS</h3>



<ul class="wp-block-list"><li>No prior HTML or CSS knowledge is required</li><li>Set up a local development environment</li><li>Use newer, modern HTML5 features &amp; specification</li><li>Learn about responsive &amp; mobile design</li><li>Create a web page project from scratch using HTML &amp; CSS</li></ul>



<h4 class="wp-block-heading">Course Links:</h4>



<ul class="wp-block-list"><li><a href="https://github.com/iamshaunjp/html-and-css-crash-course" target="_blank" rel="noreferrer noopener">Course files</a></li></ul>



<h3 class="wp-block-heading">What is HTML?</h3>



<p><strong>H</strong>yper<strong>t</strong>ext&nbsp;<strong>M</strong>arkup&nbsp;<strong>L</strong>anguage (超文本標記語言)</p>



<ul class="wp-block-list"><li>Used to structure content on a web page (images, text, forms etc)</li><li>We structure content using&nbsp;<strong><em>HTML tags</em></strong></li></ul>



<pre class="wp-block-code"><code>&lt;p&gt; content &lt;/p&gt;
&lt;a&gt; link &lt;/a&gt;
&lt;img&gt;</code></pre>



<h3 class="wp-block-heading">What is CSS?</h3>



<p><strong>C</strong>ascading&nbsp;<strong>S</strong>tyle&nbsp;<strong>S</strong>heets (階層式樣式表)</p>



<ul class="wp-block-list"><li>Works alongside HTML</li><li>Used to style web pages to make them look better<ul><li>Change colours, position, effects, font sizes etc</li></ul></li></ul>



<h4 class="wp-block-heading">程式編輯器</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://code.visualstudio.com/" target="_blank">VS Code download</a></li><li>Sublime Text</li><li>Atom</li><li>Notepad++</li></ul>



<h4 class="wp-block-heading">網頁瀏覽器</h4>



<ul class="wp-block-list"><li><a href="https://www.google.com/intl/zh-TW/chrome/" target="_blank" rel="noreferrer noopener">Google Chrome</a></li></ul>



<h4 class="wp-block-heading">打開資料夾</h4>



<ul class="wp-block-list"><li>點擊 Open Folder 選擇要開啟的資料夾</li></ul>



<h4 class="wp-block-heading">新增檔案方式</h4>



<ul class="wp-block-list"><li>右鍵 New File</li><li>左鍵點擊 New File 圖示</li></ul>



<p>檔案命名為 index.html。</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Learning HTML&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;p&gt;hello, world&lt;/p&gt;
    &lt;p&gt;my first web page!&lt;/p&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">Extensioins</h4>



<ul class="wp-block-list"><li>安裝 Live Server</li><li>在 index.html 檔案中右鍵 Open with Live Server</li></ul>



<h4 class="wp-block-heading">在瀏覽器檢視程式碼</h4>



<ul class="wp-block-list"><li>在網頁畫面中點擊右鍵檢查(Inspect)</li></ul>



<h3 class="wp-block-heading">Summary</h3>



<ul class="wp-block-list"><li>HTML is markup language to structure content</li><li>CSS is the language used to style web pages</li><li>How to create HTML file using VS Code as our editor</li><li>Seen what HTML tags are and how we use them</li><li>Know that the visible part of a web page goes in the &lt;body&gt; tag</li><li>Seen how to preview the HTML page in a browser</li></ul>



<h2 class="wp-block-heading">#02 – HTML Basics</h2>



<p>In this HTML tutorial I’ll show you the basics of HTML syntaxt and how to construct HTML tags &amp; documents.</p>



<h4 class="wp-block-heading">HTML Tags</h4>



<ul class="wp-block-list"><li>&lt;strong&gt;</li><li>&lt;em&gt;</li><li>&lt;small&gt;</li><li>&lt;h1&gt;~ &lt;h6&gt;</li><li>&lt;ul&gt;、&lt;li&gt;</li><li>&lt;ol&gt;、&lt;li&gt;</li><li>&lt;div&gt;</li><li>&lt;span&gt;</li><li>&lt;br&gt;</li><li>&lt;hr&gt;</li><li>&lt;img&gt;</li><li>&lt;a&gt;</li><li>&lt;blockquote&gt;</li><li>&lt;!–&gt;</li></ul>



<h4 class="wp-block-heading">快捷鍵</h4>



<ul class="wp-block-list"><li>Emmet abbreviation：HTML Tags + tab</li><li>註釋：Ctrl + /</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Learning HTML&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;div&gt;
      &lt;p&gt;hello, &lt;strong&gt;world&lt;/strong&gt;&lt;/p&gt;
      &lt;p&gt;my &lt;small&gt;first&lt;/small&gt; web &lt;em&gt;page!&lt;/em&gt;&lt;/p&gt;
      &lt;a href="https://www.thenetninja.co.uk/"&gt;the net ninja website&lt;/a&gt;
      &lt;a href="about.html"&gt;about page&lt;/a&gt;
    &lt;/div&gt;

    &lt;div&gt;
      &lt;h1&gt;heading 1&lt;/h1&gt;
      &lt;h2&gt;heading 2&lt;/h2&gt;
      &lt;h3&gt;heading 3&lt;/h3&gt;
      &lt;h4&gt;heading 4&lt;/h4&gt;
    &lt;/div&gt;

    &lt;div&gt;
      &lt;ul&gt;
        &lt;li&gt;yoshi&lt;/li&gt;
        &lt;li&gt;luigi&lt;/li&gt;
        &lt;li&gt;toad&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

    &lt;div&gt;
      &lt;ol&gt;
        &lt;li&gt;yoshi&lt;/li&gt;
        &lt;li&gt;luigi&lt;/li&gt;
        &lt;li&gt;toad&lt;/li&gt;
      &lt;/ol&gt;
    &lt;/div&gt;

    &lt;!-- span tag below --&gt;
    &lt;span&gt;I am a &lt;br&gt;span tag&lt;/span&gt;
    
    &lt;hr&gt;

    &lt;img src="img/ninja.png" alt="a picture of a ninja"&gt;

    &lt;blockquote cite="http://www.oscarwildesite.com/"&gt;
      we are all in the gutter, but some of us are looking at the stars
    &lt;/blockquote&gt;

    &lt;p style="color: orange;"&gt;style me :)&lt;/p&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// about.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Learning HTML&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h1&gt;about us&lt;/h1&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">Summary</h3>



<ul class="wp-block-list"><li>Learnt some of the most common HTML tags in web pages<ul><li>p, ul, li, div, a, img, etc</li></ul></li><li>Talked about attributes (src, href, cite ,alt etc)</li><li>How to create comments in an HTML file</li></ul>



<h2 class="wp-block-heading">#03 – HTML Forms</h2>



<p>In this HTML tutorial I’ll explain how to create forms in HTML, (using some newer HTML 5 input fields too). We’ll look at email fields, text fields, password fields and more.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;HTML Forms&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h1&gt;HTML Forms&lt;/h1&gt;

    &lt;form action=""&gt;
      &lt;label for="username"&gt;enter username:&lt;/label&gt;
      &lt;input type="text" id="username" name="username" placeholder="username" required&gt;
      &lt;br&gt;&lt;br&gt;
      &lt;label for="email"&gt;enter email:&lt;/label&gt;
      &lt;input type="email" id="email" name="email" placeholder="your email" required&gt;
      &lt;br&gt;&lt;br&gt;
      &lt;label for="password"&gt;password:&lt;/label&gt;
      &lt;input type="password" id="password" name="password" placeholder="choose a password" required&gt;

      &lt;p&gt;Select your age:&lt;/p&gt;

      &lt;input type="radio" name="age" value="0-25" id="option-1"&gt;
      &lt;label for="option-1"&gt;0-25&lt;/label&gt;
      &lt;input type="radio" name="age" value="26-50" id="option-2"&gt;
      &lt;label for="option-2"&gt;26-50&lt;/label&gt;
      &lt;input type="radio" name="age" value="51+" id="option-3"&gt;
      &lt;label for="option-3"&gt;51+&lt;/label&gt;

      &lt;br&gt;&lt;br&gt;

      &lt;label for="question"&gt;Security question:&lt;/label&gt;
      &lt;select name="question" id="question"&gt;
        &lt;option value="q1"&gt;What colour are your favourite pair of socks?&lt;/option&gt;
        &lt;option value="q2"&gt;If you could be a vegetable, what it be?&lt;/option&gt;
        &lt;option value="q3"&gt;What is your best ever security question?&lt;/option&gt;
      &lt;/select&gt;

      &lt;br&gt;&lt;br&gt;

      &lt;label for="answer"&gt;Security question answer:&lt;/label&gt;
      &lt;input type="text" id="answer" name="answer"&gt;

      &lt;br&gt;&lt;br&gt;

      &lt;label for="bio"&gt;Your bio:&lt;/label&gt;&lt;br&gt;
      &lt;textarea name="bio" id="bio" cols="30" rows="10" placeholder="about your..."&gt;&lt;/textarea&gt;

      &lt;br&gt;&lt;br&gt;

      &lt;input type="submit" value="submit the form"&gt;

    &lt;/form&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">快捷鍵</h4>



<ul class="wp-block-list"><li>多處程式碼一次輸入：alt + 滑鼠左鍵</li></ul>



<h4 class="wp-block-heading">補充文件</h4>



<ul class="wp-block-list"><li><a href="https://www.w3schools.com/html/html_form_input_types.asp" target="_blank" rel="noreferrer noopener">HTML Input Types</a></li></ul>



<h3 class="wp-block-heading">Summary</h3>



<ul class="wp-block-list"><li>How to create a web form and what the ‘action’ attribute is for</li><li>Seen a lot of the most common input types<ul><li>text, email, password, radio, select, textarea</li></ul></li><li>How to use labels, the id attribute &amp; the name attribute</li><li>How forms are submitted and validated</li></ul>



<h2 class="wp-block-heading">#04 – CSS Basics</h2>



<p>In this CSS tutorial for beginners we’ll have a look at the basic syntax of CSS and how we can use it to make our web pages look better.</p>



<h3 class="wp-block-heading">CSS (Style Sheets)</h3>



<p><strong>style sheet</strong>&nbsp;– a list of CSS rules / rule sets</p>



<pre class="wp-block-code"><code>// 範例程式碼
div {
  color: red;
  margin: 20px;
}

p {
  font-weight: bold;
}</code></pre>



<h4 class="wp-block-heading">selectors (選擇器)</h4>



<ul class="wp-block-list"><li>div</li><li>p</li></ul>



<h4 class="wp-block-heading">declarations (宣告)</h4>



<ul class="wp-block-list"><li>color: red;</li><li>margin: 20px;</li><li>font-weight: bold;</li></ul>



<p><a href="https://developer.mozilla.org/zh-TW/docs/Learn/Getting_started_with_the_web/CSS_basics" target="_blank" rel="noreferrer noopener">CSS 基本 – MDN文件</a></p>



<pre class="wp-block-code"><code>// index.html - 1

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h1&gt;CSS Basics&lt;/h1&gt;

    &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit aliquid obcaecati neque sunt beatae quis saepe asperiores ea nobis dolorem. Unde quis nobis facere iure, veritatis placeat iusto ratione facilis!&lt;/p&gt;
    
    &lt;div&gt;
      &lt;h2&gt;Ninja available for hire&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit id labore, similique quod hic reprehenderit delectus quaerat reiciendis maiores pariatur, nam cum harum non aliquam, vel cumque quae! Quisquam, architecto.&lt;/p&gt;
      &lt;ul&gt;
        &lt;li&gt;Ninja Yoshi&lt;/li&gt;
        &lt;li&gt;Ninja Mario&lt;/li&gt;
        &lt;li&gt;Ninja Shaun&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// about.html - 1

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;About me&lt;/h1&gt;

    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed necessitatibus fugit ducimus hic qui eligendi repellat debitis rem molestias reiciendis, quos commodi possimus voluptatum provident natus, ipsam veritatis tenetur doloribus.&lt;/p&gt;
    
  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.html - 2

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;style&gt;
      h1 {
        color: orange;
      }
      p {
        color: slategray;
      }
    &lt;/style&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h1&gt;CSS Basics&lt;/h1&gt;

    &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit aliquid obcaecati neque sunt beatae quis saepe asperiores ea nobis dolorem. Unde quis nobis facere iure, veritatis placeat iusto ratione facilis!&lt;/p&gt;
    
    &lt;div&gt;
      &lt;h2&gt;Ninja available for hire&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit id labore, similique quod hic reprehenderit delectus quaerat reiciendis maiores pariatur, nam cum harum non aliquam, vel cumque quae! Quisquam, architecto.&lt;/p&gt;
      &lt;ul&gt;
        &lt;li&gt;Ninja Yoshi&lt;/li&gt;
        &lt;li&gt;Ninja Mario&lt;/li&gt;
        &lt;li&gt;Ninja Shaun&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// about.html - 2

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;style&gt;
      h1{
        color: orange;
      }
      p{
        color: slategray;
      }
    &lt;/style&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;About me&lt;/h1&gt;

    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed necessitatibus fugit ducimus hic qui eligendi repellat debitis rem molestias reiciendis, quos commodi possimus voluptatum provident natus, ipsam veritatis tenetur doloribus.&lt;/p&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// index.html - 3

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h1&gt;CSS Basics&lt;/h1&gt;

    &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit aliquid obcaecati neque sunt beatae quis saepe asperiores ea nobis dolorem. Unde quis nobis facere iure, veritatis placeat iusto ratione facilis!&lt;/p&gt;
    
    &lt;div&gt;
      &lt;h2&gt;Ninja available for hire&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit id labore, similique quod hic reprehenderit delectus quaerat reiciendis maiores pariatur, nam cum harum non aliquam, vel cumque quae! Quisquam, architecto.&lt;/p&gt;
      &lt;ul&gt;
        &lt;li&gt;Ninja Yoshi&lt;/li&gt;
        &lt;li&gt;Ninja Mario&lt;/li&gt;
        &lt;li&gt;Ninja Shaun&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css - 3

h1 {
  color: orange;

  background-color: slategray;
  font-size: 20px;
  text-decoration: underline;
  font-family: arial;
  text-align: center;
}
p {
  color: slategray;
}</code></pre>



<pre class="wp-block-code"><code>// about.html - 3

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;About me&lt;/h1&gt;

    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed necessitatibus fugit ducimus hic qui eligendi repellat debitis rem molestias reiciendis, quos commodi possimus voluptatum provident natus, ipsam veritatis tenetur doloribus.&lt;/p&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">text-decoration</h4>



<ul class="wp-block-list"><li>line-through</li><li>underline</li><li>none</li></ul>



<h4 class="wp-block-heading">CSS Web Safe Fonts</h4>



<ul class="wp-block-list"><li><a href="https://www.w3schools.com/cssref/css_websafe_fonts.asp" target="_blank" rel="noreferrer noopener">Web safe fonts</a></li></ul>



<h4 class="wp-block-heading">text-align</h4>



<ul class="wp-block-list"><li>left</li><li>right</li><li>center</li></ul>



<h4 class="wp-block-heading">light-height</h4>



<ul class="wp-block-list"><li>30px</li></ul>



<h4 class="wp-block-heading">letter-spacing</h4>



<ul class="wp-block-list"><li>3px</li></ul>



<h4 class="wp-block-heading">column-count</h4>



<ul class="wp-block-list"><li>3</li></ul>



<h4 class="wp-block-heading">column-gap</h4>



<ul class="wp-block-list"><li>60px</li></ul>



<h4 class="wp-block-heading">border</h4>



<ul class="wp-block-list"><li>border-width: 4px;</li><li>border-style: solid;</li><li>border-color: crimson;</li><li>border: 4px solid crimson;</li></ul>



<ul class="wp-block-list"><li>border-left</li><li>border-top</li><li>border-right</li><li>border-bottom</li></ul>



<ul class="wp-block-list"><li>solid</li><li>dashed</li><li>dotted</li></ul>



<h4 class="wp-block-heading">comment</h4>



<ul class="wp-block-list"><li>/* */</li></ul>



<h4 class="wp-block-heading">list-style-type</h4>



<ul class="wp-block-list"><li>disc</li><li>square</li><li>none</li></ul>



<h4 class="wp-block-heading">text-shadow</h4>



<ul class="wp-block-list"><li>2px 2px lightgray;</li></ul>



<pre class="wp-block-code"><code>// style.css - 4

h1 {
  color: orange;
  background-color: slategray;
  font-size: 20px;
  text-decoration: underline;
  font-family: arial;
  text-align: center;
}
p {
  color: slategray;
  text-align: right;
  line-height: 30px;
  letter-spacing: 3px;
  column-count: 3;
  column-gap: 60px;
}
ul {
  /* border-width: 4px;
  border-style: solid;
  border-color: crimson; */
  /* border: 4px solid crimson; */
  border-bottom: 4px solid crimson;
  border-left: 8px dashed crimson;
}
li {
  list-style-type: disc;
  text-shadow: 2px 2px lightgray;
}</code></pre>



<h3 class="wp-block-heading">Colours / Hex Codes</h3>



<ul class="wp-block-list"><li>white、black、yellow、green</li><li>#42f4f1、#ab55ef</li></ul>



<h4 class="wp-block-heading">#42f4f1</h4>



<ul class="wp-block-list"><li>42 – R</li><li>f4 – G</li><li>f1 – B</li></ul>



<pre class="wp-block-code"><code>// style.css - 5

h1 {
  color: orange;
  background-color: slategray;
  font-size: 20px;
  text-decoration: underline;
  font-family: arial;
  text-align: center;
}
p {
  color: slategray;
  text-align: right;
  line-height: 30px;
  letter-spacing: 3px;
  column-count: 3;
  column-gap: 60px;
}
ul {
  /* border-width: 4px;
  border-style: solid;
  border-color: crimson; */
  /* border: 4px solid crimson; */
  border-bottom: 4px solid crimson;
  border-left: 8px dashed #25bb2d;
}
li {
  list-style-type: disc;
  /* text-shadow: 2px 2px lightgray; */
  text-shadow: 2px 2px #e9e9e9;
}</code></pre>



<h3 class="wp-block-heading">Inline Elements</h3>



<ul class="wp-block-list"><li>Don’t take up any more room than their content needs</li><li>span, img, strong, em, a and more…</li></ul>



<h3 class="wp-block-heading">Block-level Elements</h3>



<ul class="wp-block-list"><li>Take up the whole width of a page regardless of content</li><li>p, div, h1, h2, h3, ul, li and more…</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h2&gt;Inline elements&lt;/h2&gt;
    
    &lt;span&gt;span tag&lt;/span&gt;
    &lt;em&gt;em tag&lt;/em&gt;
    &lt;a href=""&gt;anchor tag&lt;/a&gt;
    &lt;span&gt;span tag&lt;/span&gt;

    &lt;h2&gt;Block-level elements&lt;/h2&gt;

    &lt;div&gt;div tag&lt;/div&gt;
    &lt;div&gt;another div tag&lt;/div&gt;
    &lt;h3&gt;h3 tag&lt;/h3&gt;
    &lt;p&gt;paragraph tag&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;li tag&lt;/li&gt;
      &lt;li&gt;another li tag&lt;/li&gt;
    &lt;/ul&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">Inline、Block-level elements 區分</h4>



<ul class="wp-block-list"><li>使用 Google Elements 查看元素</li><li>透過 Google Styles 查看是否有 display: block;</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h2&gt;Inline elements&lt;/h2&gt;
    
    &lt;span&gt;span tag&lt;/span&gt;
    &lt;em&gt;em tag&lt;/em&gt;
    &lt;a href=""&gt;anchor tag&lt;/a&gt;
    &lt;span&gt;span tag&lt;/span&gt;

    &lt;h2&gt;Block-level elements&lt;/h2&gt;

    &lt;div&gt;div tag&lt;/div&gt;
    &lt;div&gt;another div tag&lt;/div&gt;
    &lt;h3&gt;h3 tag&lt;/h3&gt;
    &lt;p&gt;paragraph tag&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;li tag&lt;/li&gt;
      &lt;li&gt;another li tag&lt;/li&gt;
    &lt;/ul&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css - Inline、Block-level elements 修改

span {
  display: block;
}
div {
  display: inline;
}</code></pre>



<h3 class="wp-block-heading">Margin &amp; Padding</h3>



<figure class="wp-block-gallery has-nested-images columns-1 is-cropped wp-block-gallery-8 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1494" height="679" data-id="474" src="/wordpress_blog/wp-content/uploads/2022/04/div-marinpadding.jpg" alt="" class="wp-image-474"/><figcaption>div &#8211; margin &amp; padding</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="952" height="496" data-id="475" src="/wordpress_blog/wp-content/uploads/2022/04/span-marginpadding.jpg" alt="" class="wp-image-475"/><figcaption>span &#8211; margin &amp; padding</figcaption></figure>
</figure>



<ul class="wp-block-list"><li>span – margin 只能左右增加空間。</li><li>margin collapse – 邊界重疊</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;h2&gt;Inline elements&lt;/h2&gt;
    
    &lt;span&gt;span tag&lt;/span&gt;
    &lt;em&gt;em tag&lt;/em&gt;
    &lt;a href=""&gt;anchor tag&lt;/a&gt;
    &lt;span&gt;span tag&lt;/span&gt;

    &lt;h2&gt;Block-level elements&lt;/h2&gt;

    &lt;div&gt;div tag&lt;/div&gt;
    &lt;div&gt;another div tag&lt;/div&gt;
    &lt;h3&gt;h3 tag&lt;/h3&gt;
    &lt;p&gt;paragraph tag&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;li tag&lt;/li&gt;
      &lt;li&gt;another li tag&lt;/li&gt;
    &lt;/ul&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css - 1

span {
  /* display: block; */
  margin: 20px;
  padding: 20px;
}
div {
  /* display: inline; */
  border: 2px solid crimson;
  margin: 20px;
  padding: 20px;
}</code></pre>



<pre class="wp-block-code"><code>// style.css - 2

span {
  display: block;
  margin: 20px;
  padding: 20px;
}
div {
  /* display: inline; */
  border: 2px solid crimson;
  margin: 20px;
  padding: 20px;
}</code></pre>



<h4 class="wp-block-heading">Default Browser Styles (瀏覽器預設樣式)</h4>



<h3 class="wp-block-heading">Summary</h3>



<ul class="wp-block-list"><li>What CSS is and how to add it to a webpage</li><li>Basic selectors and some different declarations / properties</li><li>Hex codes and the VS Code colour picker</li><li>Inline and Block-level elements (and inline-block)</li><li>Margin &amp; Padding</li><li>Default browser styles</li></ul>



<h2 class="wp-block-heading">#05 – CSS Classes &amp; Selectors</h2>



<h4 class="wp-block-heading">ID</h4>



<ul class="wp-block-list"><li>一個頁面每個 id 名稱只能有一個</li><li>絕大部分我們只會使用在 JavaScript</li></ul>



<h4 class="wp-block-heading">a[href]</h4>



<ul class="wp-block-list"><li>a[href=”https://www.google.com”]/</li><li>a[href*=”google”]</li><li>a[href$=”.com”]</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;div id="content"&gt;

      &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Optio quae eos quaerat a assumenda nesciunt dolorum necessitatibus ea tenetur accusantium vel aspernatur maxime ipsa, rem, reprehenderit excepturi, veritatis eligendi laudantium.&lt;/p&gt;

      &lt;p class="error"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam, eius ullam corrupti nam modi nemo, necessitatibus officia laboriosam quaerat explicabo provident, temporibus nobis aperiam ipsam sequi accusamus iusto? Ut, exercitationem.&lt;/p&gt;

      &lt;p class="success"&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Dignissimos non culpa tempore. Voluptatum maxime, et doloribus repellat cupiditate ipsum, consectetur qui iure quod obcaecati, inventore voluptatem fuga! Tempora, similique velit?&lt;/p&gt;

      &lt;p class="success feedback"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim cum harum et, vitae reprehenderit iure error, amet eius facere ullam odio porro! In deleniti expedita nisi tempora temporibus illum rerum.&lt;/p&gt;

    &lt;/div&gt;

    &lt;p&gt;Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptas, voluptatum nemo, quae molestias expedita saepe dicta velit iste esse temporibus, ex labore perferendis ratione eos. Et blanditiis eius fugiat suscipit.&lt;/p&gt;

    &lt;div class="error"&gt;I am an error&lt;/div&gt;

    &lt;a href="https://www.thenetninja.co.uk/"&gt;the best site on the web&lt;/a&gt;
    &lt;a href="https://www.google.com/"&gt;Google Website&lt;/a&gt;
    &lt;a href="https://www.thenetninja.co.uk/"&gt;the best site on the web&lt;/a&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

/* p.error {
  color: red;
}
p.success {
  color: #4fdb4f;
}
p.success.feedback {
  border: 1px dashed #4fdb4f;
}
#content {
  background-color: #ebebeb;
  padding: 20px;
} */

/* div .error{
  color: red;
} */

a&#91;href*="thenetninja"]{
  background-color: #3dd13d;
  color: white;
  text-decoration: none;
  font-weight: bold;
  text-transform: uppercase;
}
a&#91;href$=".com"]{
  color: white;
  background: red;
  border: 2px solid blue;
}</code></pre>



<h3 class="wp-block-heading">The Cascade</h3>



<h3 class="wp-block-heading">Inheritance(繼承)</h3>



<ul class="wp-block-list"><li>HTML elements can inherit CSS properties that are applied to their parents</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
    &lt;title&gt;CSS Basics&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    
    &lt;div id="content"&gt;
      ninja
      &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Optio quae eos quaerat a assumenda nesciunt dolorum necessitatibus ea tenetur accusantium vel aspernatur maxime ipsa, rem, reprehenderit excepturi, veritatis eligendi laudantium.&lt;/p&gt;

      &lt;p class="error"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam, eius ullam corrupti nam modi nemo, necessitatibus officia laboriosam quaerat explicabo provident, temporibus nobis aperiam ipsam sequi accusamus iusto? Ut, exercitationem.&lt;/p&gt;

      &lt;p class="success"&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Dignissimos non culpa tempore. Voluptatum maxime, et doloribus repellat cupiditate ipsum, consectetur qui iure quod obcaecati, inventore voluptatem fuga! Tempora, similique velit?&lt;/p&gt;

      &lt;p class="success feedback"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim cum harum et, vitae reprehenderit iure error, amet eius facere ullam odio porro! In deleniti expedita nisi tempora temporibus illum rerum.&lt;/p&gt;

    &lt;/div&gt;

    &lt;p&gt;Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptas, voluptatum nemo, quae molestias expedita saepe dicta velit iste esse temporibus, ex labore perferendis ratione eos. Et blanditiis eius fugiat suscipit.&lt;/p&gt;

    &lt;div class="error"&gt;I am an error&lt;/div&gt;

    &lt;a href="https://www.thenetninja.co.uk/"&gt;the best site on the web&lt;/a&gt;
    &lt;a href="https://www.google.com/"&gt;Google Website&lt;/a&gt;
    &lt;a href="https://www.thenetninja.co.uk/"&gt;the best site on the web&lt;/a&gt;

  &lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

/* p.error {
  color: red;
}
p.success {
  color: #4fdb4f;
}
p.success.feedback {
  border: 1px dashed #4fdb4f;
}
#content {
  background-color: #ebebeb;
  padding: 20px;
} */

/* div .error{
  color: red;
} */

/* a&#91;href*="thenetninja"]{
  background-color: #3dd13d;
  color: white;
  text-decoration: none;
  font-weight: bold;
  text-transform: uppercase;
}
a&#91;href$=".com"]{
  color: white;
  background: red;
  border: 2px solid blue;
} */

div{
  color: lightcoral;
  border: 1px solid gray;
  margin: 40px;
  font-weight: bold;
}
div p{
  border: inherit;
  margin: inherit;
  color: crimson;
}
p{
  color: orange;
}</code></pre>



<ul class="wp-block-list"><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade" target="_blank" rel="noreferrer noopener">More about the cascade</a></li></ul>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#06 – HTML 5 Semantics</h2>



<p>In this HTML &amp; CSS tutorial for beginners we’ll take a look at some of the newer HTML semantic tags, like section, article, header, nav and aside.</p>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://www.w3schools.com/html/html5_semantic_elements.asp" target="_blank">HTML 5 Semantic tags, more information</a></li><li><a href="https://github.com/iamshaunjp/html-and-css-crash-course" target="_blank" rel="noreferrer noopener">Course files</a></li></ul>



<h3 class="wp-block-heading">HTML5 Semantic Tags</h3>



<pre class="wp-block-code"><code>&lt;article&gt;
  &lt;p&gt;some content&lt;/p&gt;
  &lt;p&gt;some more content&lt;/p&gt;
&lt;/article&gt;</code></pre>



<ul class="wp-block-list"><li>&lt;main&gt;<br>For the main content of a webpage, unique to that page</li><li>&lt;section&gt;<br>Defines a certain section of a webpage (e.g. blog list, contact info)</li><li>&lt;article&gt;<br>Defines a bit of content which makes up an article (e.g. a blog post)</li><li>&lt;aside&gt;<br>Defines some content related to something else (e.g. similar blogs)</li><li>&lt;header&gt;<br>For the header of a website – contains the nav, title etc</li><li>&lt;footer&gt;<br>For the footer of a website</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Marioclub&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;header&gt;
    &lt;h1&gt;Marioclub&lt;/h1&gt;
  &lt;/header&gt;
  &lt;section class="banner"&gt;
    &lt;img src="img/banner.png" alt="marioclub welcome banner"&gt;
    &lt;div class="welcome"&gt;
      &lt;h2&gt;Welcome to &lt;br&gt;&lt;span&gt;Marioclub&lt;/span&gt;&lt;/h2&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;nav class="main-nav"&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href="/join.html" class="join"&gt;Join the club&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/news.html"&gt;Latest news&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/games.html"&gt;New games&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/contact.html"&gt;Contact&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/nav&gt;
  &lt;main&gt;
    &lt;article&gt;
      &lt;h2&gt;It's a me, Mario&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident modi vitae voluptatibus nulla fugiat nihil ut! Natus, repellendus asperiores veritatis non assumenda hic veniam aperiam vitae doloribus eius fugit magni?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat sunt voluptates ullam, neque vitae aperiam! Corporis odio exercitationem iure fugiat cum. A obcaecati molestias officiis aperiam ad velit dolor non?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cum modi quas iste accusantium dolorem a! Sit tenetur, assumenda illo facilis culpa dolore dolores molestiae amet possimus autem nobis quasi recusandae.&lt;/p&gt;
    &lt;/article&gt;
    &lt;ul class="images"&gt;
      &lt;li&gt;&lt;img src="img/thumb-1.png" alt="mario thumb 1"&gt;&lt;/li&gt;
      &lt;li&gt;&lt;img src="img/thumb-2.png" alt="mario thumb 2"&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/main&gt;
  &lt;section class="join"&gt;
    &lt;h2&gt;Join Today!&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Assumenda itaque deleniti maxime nesciunt quod recusandae dolorem, aperiam quis enim&lt;/p&gt;
    &lt;form&gt;
      &lt;input type="email" name="email" placeholder="Type email &amp; hit enter" required&gt;
    &lt;/form&gt;
  &lt;/section&gt;
  &lt;footer&gt;
    &lt;p class="copyright"&gt;Copyright 2019 Marioclub&lt;/p&gt;
  &lt;/footer&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#07 – Chrome Dev Tools</h2>



<p>In this HTML &amp; CSS tutorial for beginners I’ll go through what Google Chrome dev tools are – and how they can help us when creating websites. We’ll look at inspecting elements, getting CSS selectors, editing attributes &amp; CSS rules on the fly and also importing project folders into Chrome for live coding.</p>



<h4 class="wp-block-heading">打開 Chrome Dev Tools</h4>



<ul class="wp-block-list"><li>滑鼠右鍵、檢查</li><li>直接按 F12</li></ul>



<h4 class="wp-block-heading">講解 Chrome Dev Tools 功能</h4>



<ul class="wp-block-list"><li>Chrome Elements<ul><li>Elements</li></ul><ul><li>Copy → Copy Selector</li><li>Edit as HTML</li><li>Scroll into view</li><li>Hide element</li><li>Delete element</li><li>Add attribute</li><li>Styles</li><li>Select an element in the page to inspect it</li></ul></li><li>Chrome Sources<ul><li>Page</li><li>Filesystem</li></ul></li><li>Chrome Console</li><li>Toggle device toolbar</li></ul>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#08 – CSS Layout &amp; Position</h2>



<p>In this HTML &amp; CSS Tutorial we’ll look at how to layout our content on the page. We’ll do this by using a mixture of display types (block, inline &amp; inline-block) as well as looking in detail at the ‘position’ property.</p>



<h3 class="wp-block-heading">Position &amp; Layout</h3>



<ul class="wp-block-list"><li>Static</li><li>Relative</li><li>Fixed</li><li>Absolute</li><li>Sticky<ul><li>… A mixture of ‘static’ and ‘fixed’</li></ul></li></ul>



<pre class="wp-block-code"><code>// Relative

{
  position: relative;
  left: 20px;
  bottom: 20px;
}</code></pre>



<pre class="wp-block-code"><code>// Fiexed

{
  position: fixed;
  left: 0px;
  top: 0px;
}</code></pre>



<pre class="wp-block-code"><code>// Absolute

{
  position: absolute;
  left: 20px;
  bottom: 20px;
}</code></pre>



<h4 class="wp-block-heading">專案範例 – Marioclub</h4>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Marioclub&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;header&gt;
    &lt;h1&gt;Marioclub&lt;/h1&gt;
  &lt;/header&gt;
  &lt;section class="banner"&gt;
    &lt;img src="img/banner.png" alt="marioclub welcome banner"&gt;
    &lt;div class="welcome"&gt;
      &lt;h2&gt;Welcome to &lt;br&gt;&lt;span&gt;Marioclub&lt;/span&gt;&lt;/h2&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;nav class="main-nav"&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href="/join.html" class="join"&gt;Join the club&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/news.html"&gt;Latest news&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/games.html"&gt;New games&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/contact.html"&gt;Contact&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/nav&gt;
  &lt;main&gt;
    &lt;article&gt;
      &lt;h2&gt;It's a me, Mario&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident modi vitae voluptatibus nulla fugiat nihil ut! Natus, repellendus asperiores veritatis non assumenda hic veniam aperiam vitae doloribus eius fugit magni?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat sunt voluptates ullam, neque vitae aperiam! Corporis odio exercitationem iure fugiat cum. A obcaecati molestias officiis aperiam ad velit dolor non?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cum modi quas iste accusantium dolorem a! Sit tenetur, assumenda illo facilis culpa dolore dolores molestiae amet possimus autem nobis quasi recusandae.&lt;/p&gt;
    &lt;/article&gt;
    &lt;ul class="images"&gt;
      &lt;li&gt;&lt;img src="img/thumb-1.png" alt="mario thumb 1"&gt;&lt;/li&gt;
      &lt;li&gt;&lt;img src="img/thumb-2.png" alt="mario thumb 2"&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/main&gt;
  &lt;section class="join"&gt;
    &lt;h2&gt;Join Today!&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Assumenda itaque deleniti maxime nesciunt quod recusandae dolorem, aperiam quis enim&lt;/p&gt;
    &lt;form&gt;
      &lt;input type="email" name="email" placeholder="Type email &amp; hit enter" required&gt;
    &lt;/form&gt;
  &lt;/section&gt;
  &lt;footer&gt;
    &lt;p class="copyright"&gt;Copyright 2019 Marioclub&lt;/p&gt;
  &lt;/footer&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

body, ul, li, h1, h2, a{
  margin: 0;
  padding: 0;
  font-family: arial;
}

header{
  background-color: #F63232;
  padding: 20px;
  text-align: center;
  position: fixed;
  width: 100%;
  z-index: 1;
  top: 0;
  left: 0;
}
header h1{
  color: white;
  border: 8px solid white;
  padding: 6px 12px;
  display: inline-block;
  border-radius: 36px;
}
.banner{
  position: relative;
}
.banner img{
  max-width: 100%;
}
.banner .welcome{
  background-color: #FEB614;
  color: white;
  padding: 30px;
  position: absolute;
  left: 0;
  /* 設定 % 可自適應延伸 */
  top: 30%; 
}
.banner h2{
  font-size: 74px;
}
.banner h2 span{
  font-size: 1.3em;
}
nav{
  background-color: #F4F4F4;
  padding: 20px;
  position: sticky;
  /* top: 148px; */
  /* top: 147px; */
  /* top: 106px; */
  top: 104px;
}
nav ul{
  white-space: nowrap;
  max-width: 1200px;
  margin: 0 auto;
}
nav li{
  width: 25%;
  display: inline-block;
  font-size: 24px;
}
nav li a{
  text-decoration: none;
  color: #4B4B4B;
}
nav li a.join{
  color: #F63232;
}
main{
  max-width: 100%;
  width: 1200px;
  margin: 80px auto;
  padding: 0 40px;
  box-sizing: border-box;
}
article h2{
  color: #F63232;
  font-size: 48px;
}
article p{
  line-height: 2em;
  color: #4B4B4B;
}
.images{
  text-align: center;
  margin: 80px 0;
  white-space: nowrap;
}
.images li{
  display: inline-block;
  width: 40%;
  margin: 20px 5%;
}
.images li img{
  max-width: 100%;
}
section.join{
  background: #F4F4F4;
  text-align: center;
  padding: 60px 20px;
  color: #4B4B4B;
}
.join h2{
  font-size: 36px;
}
form input{
  margin: 20px 0;
  padding: 10px 20px;
  font-size: 24px;
  border-radius: 28px;
  border: 4px solid white;
}
footer{
  background: #F63232;
  color: white;
  padding: 10px;
  text-align: center;
}</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#09 – Pseudo Classes &amp; Elements (虛擬類別、偽類別 &amp; 虛擬元素、偽元素)</h2>



<p>In this HTML &amp; CSS tutorial for beginners we’ll talk about pseudo classes and pseudo elements. These two things allow us to create more powerful &amp; flexible selectors in our CSS as well as target elements in a particular state (e.g. hover state, active state or focus state).</p>



<ul class="wp-block-list"><li><a href="https://www.w3schools.com/css/css_pseudo_elements.asp" target="_blank" rel="noreferrer noopener">List of pseudo classes / elements</a></li></ul>



<h3 class="wp-block-heading">Pseudo Classes</h3>



<ul class="wp-block-list"><li>Style elements when they’re in a particular state<ul><li>hover, focus, first child of a parent</li></ul></li><li>:hover</li><li>:focus</li><li>:first-child</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Marioclub&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;header&gt;
    &lt;h1&gt;Marioclub&lt;/h1&gt;
  &lt;/header&gt;
  &lt;section class="banner"&gt;
    &lt;img src="img/banner.png" alt="marioclub welcome banner"&gt;
    &lt;div class="welcome"&gt;
      &lt;h2&gt;Welcome to &lt;br&gt;&lt;span&gt;Marioclub&lt;/span&gt;&lt;/h2&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;nav class="main-nav"&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href="/join.html" class="join"&gt;Join the club&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/news.html"&gt;Latest news&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/games.html"&gt;New games&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/contact.html"&gt;Contact&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/nav&gt;
  &lt;main&gt;
    &lt;article&gt;
      &lt;h2&gt;It's a me, Mario&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident modi vitae voluptatibus nulla fugiat nihil ut! Natus, repellendus asperiores veritatis non assumenda hic veniam aperiam vitae doloribus eius fugit magni?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat sunt voluptates ullam, neque vitae aperiam! Corporis odio exercitationem iure fugiat cum. A obcaecati molestias officiis aperiam ad velit dolor non?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cum modi quas iste accusantium dolorem a! Sit tenetur, assumenda illo facilis culpa dolore dolores molestiae amet possimus autem nobis quasi recusandae.&lt;/p&gt;
    &lt;/article&gt;
    &lt;ul class="images"&gt;
      &lt;li&gt;&lt;img src="img/thumb-1.png" alt="mario thumb 1"&gt;&lt;/li&gt;
      &lt;li&gt;&lt;img src="img/thumb-2.png" alt="mario thumb 2"&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/main&gt;
  &lt;section class="join"&gt;
    &lt;h2&gt;Join Today!&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Assumenda itaque deleniti maxime nesciunt quod recusandae dolorem, aperiam quis enim&lt;/p&gt;
    &lt;form&gt;
      &lt;input type="email" name="email" placeholder="Type email &amp; hit enter" required&gt;
    &lt;/form&gt;
  &lt;/section&gt;
  &lt;footer&gt;
    &lt;p class="copyright"&gt;Copyright 2019 Marioclub&lt;/p&gt;
  &lt;/footer&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

body, ul, li, h1, h2, a{
  margin: 0;
  padding: 0;
  font-family: arial;
}

header{
  background-color: #F63232;
  padding: 20px;
  text-align: center;
  position: fixed;
  width: 100%;
  z-index: 1;
  top: 0;
  left: 0;
}
header h1{
  color: white;
  border: 8px solid white;
  padding: 6px 12px;
  display: inline-block;
  border-radius: 36px;
}
.banner{
  position: relative;
}
.banner img{
  max-width: 100%;
}
.banner .welcome{
  background-color: #FEB614;
  color: white;
  padding: 30px;
  position: absolute;
  left: 0;
  /* 設定 % 可自適應延伸 */
  top: 30%; 
}
.banner h2{
  font-size: 74px;
}
.banner h2 span{
  font-size: 1.3em;
}
nav{
  background-color: #F4F4F4;
  padding: 20px;
  position: sticky;
  /* top: 148px; */
  /* top: 147px; */
  /* top: 106px; */
  top: 104px;
}
nav ul{
  white-space: nowrap;
  max-width: 1200px;
  margin: 0 auto;
}
nav li{
  width: 25%;
  display: inline-block;
  font-size: 24px;
}
nav li a{
  text-decoration: none;
  color: #4B4B4B;
}
nav li a.join{
  color: #F63232;
}
main{
  max-width: 100%;
  width: 1200px;
  margin: 80px auto;
  padding: 0 40px;
  box-sizing: border-box;
}
article h2{
  color: #F63232;
  font-size: 48px;
}
article p{
  line-height: 2em;
  color: #4B4B4B;
}
.images{
  text-align: center;
  margin: 80px 0;
  white-space: nowrap;
}
.images li{
  display: inline-block;
  width: 40%;
  margin: 20px 5%;
}
.images li img{
  max-width: 100%;
}
section.join{
  background: #F4F4F4;
  text-align: center;
  padding: 60px 20px;
  color: #4B4B4B;
}
.join h2{
  font-size: 36px;
}
form input{
  margin: 20px 0;
  padding: 10px 20px;
  font-size: 24px;
  border-radius: 28px;
  border: 4px solid white;
}
footer{
  background: #F63232;
  color: white;
  padding: 10px;
  text-align: center;
}

/* pseudo classes */

nav li a:hover{
  text-decoration: underline;
}
.images li:hover{
  position: relative;
  top: -4px;
}
form input:focus{
  border: 4px dashed #4B4B4B;
  outline: none;
}
form input:valid{
  border: 4px solid #71D300;
}
/* 說明講解 :first-child */
/* nav li:first-child{
  border: 3px solid #F63232;
} */
article p::first-line{
  font-weight: bold;
  font-size: 1.2em;
}
section.join p::first-letter{
  font-size: 1.5em;
}
p::selection{
  background-color: #F63232;
  color: white;
}
p::after{
  content: '...';
}</code></pre>



<h4 class="wp-block-heading">偽元素</h4>



<ul class="wp-block-list"><li>::after</li><li>::before</li><li>::first-line</li><li>::first-letter</li><li>::selection</li></ul>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#10 – Intro to Media Queries</h2>



<p>In this CSS tutorial I’ll give you a quick introduction to the concept of responsive design and media queries. We’ll use those to make our Mario web page look nice on mobile devices.</p>



<h3 class="wp-block-heading">Responsive Design (響應式設計)</h3>



<h4 class="wp-block-heading">Media Queries</h4>



<p>Tell the browser how to style an element at particular viewport dimensions</p>



<h4 class="wp-block-heading">Viewport meta tag</h4>



<p>Tells the browser what width the viewport should be</p>



<h4 class="wp-block-heading">Responsive images</h4>



<p>Only load smaller images for mobile devices</p>



<h4 class="wp-block-heading">關於 Responsive Web Design 介紹</h4>



<ul class="wp-block-list"><li><a rel="noreferrer noopener" href="https://www.w3schools.com/css/css_rwd_viewport.asp" target="_blank">RWD Viewport</a></li><li><a href="https://www.w3schools.com/css/css_rwd_mediaqueries.asp" target="_blank" rel="noreferrer noopener">RWD Media Queries</a></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="style.css"&gt;
  &lt;title&gt;Marioclub&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;header&gt;
    &lt;h1&gt;Marioclub&lt;/h1&gt;
  &lt;/header&gt;
  &lt;section class="banner"&gt;
    &lt;img src="img/banner.png" alt="marioclub welcome banner"&gt;
    &lt;div class="welcome"&gt;
      &lt;h2&gt;Welcome to &lt;br&gt;&lt;span&gt;Marioclub&lt;/span&gt;&lt;/h2&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;nav class="main-nav"&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href="/join.html" class="join"&gt;Join the club&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/news.html"&gt;Latest news&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/games.html"&gt;New games&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href="/contact.html"&gt;Contact&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/nav&gt;
  &lt;main&gt;
    &lt;article&gt;
      &lt;h2&gt;It's a me, Mario&lt;/h2&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Provident modi vitae voluptatibus nulla fugiat nihil ut! Natus, repellendus asperiores veritatis non assumenda hic veniam aperiam vitae doloribus eius fugit magni?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat sunt voluptates ullam, neque vitae aperiam! Corporis odio exercitationem iure fugiat cum. A obcaecati molestias officiis aperiam ad velit dolor non?Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cum modi quas iste accusantium dolorem a! Sit tenetur, assumenda illo facilis culpa dolore dolores molestiae amet possimus autem nobis quasi recusandae.&lt;/p&gt;
    &lt;/article&gt;
    &lt;ul class="images"&gt;
      &lt;li&gt;&lt;img src="img/thumb-1.png" alt="mario thumb 1"&gt;&lt;/li&gt;
      &lt;li&gt;&lt;img src="img/thumb-2.png" alt="mario thumb 2"&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/main&gt;
  &lt;section class="join"&gt;
    &lt;h2&gt;Join Today!&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Assumenda itaque deleniti maxime nesciunt quod recusandae dolorem, aperiam quis enim&lt;/p&gt;
    &lt;form&gt;
      &lt;input type="email" name="email" placeholder="Type email &amp; hit enter" required&gt;
    &lt;/form&gt;
  &lt;/section&gt;
  &lt;footer&gt;
    &lt;p class="copyright"&gt;Copyright 2019 Marioclub&lt;/p&gt;
  &lt;/footer&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// style.css

body, ul, li, h1, h2, a{
  margin: 0;
  padding: 0;
  font-family: arial;
}

header{
  background-color: #F63232;
  padding: 20px;
  text-align: center;
  position: fixed;
  width: 100%;
  z-index: 1;
  top: 0;
  left: 0;
}
header h1{
  color: white;
  border: 8px solid white;
  padding: 6px 12px;
  display: inline-block;
  border-radius: 36px;
}
.banner{
  position: relative;
}
.banner img{
  max-width: 100%;
}
.banner .welcome{
  background-color: #FEB614;
  color: white;
  padding: 30px;
  position: absolute;
  left: 0;
  /* 設定 % 可自適應延伸 */
  top: 30%; 
}
.banner h2{
  font-size: 74px;
}
.banner h2 span{
  font-size: 1.3em;
}
nav{
  background-color: #F4F4F4;
  padding: 20px;
  position: sticky;
  /* top: 148px; */
  /* top: 147px; */
  /* top: 106px; */
  top: 104px;
}
nav ul{
  white-space: nowrap;
  max-width: 1200px;
  margin: 0 auto;
}
nav li{
  width: 25%;
  display: inline-block;
  font-size: 24px;
}
nav li a{
  text-decoration: none;
  color: #4B4B4B;
}
nav li a.join{
  color: #F63232;
}
main{
  max-width: 100%;
  width: 1200px;
  margin: 80px auto;
  padding: 0 40px;
  box-sizing: border-box;
}
article h2{
  color: #F63232;
  font-size: 48px;
}
article p{
  line-height: 2em;
  color: #4B4B4B;
}
.images{
  text-align: center;
  margin: 80px 0;
  white-space: nowrap;
}
.images li{
  display: inline-block;
  width: 40%;
  margin: 20px 5%;
}
.images li img{
  max-width: 100%;
}
section.join{
  background: #F4F4F4;
  text-align: center;
  padding: 60px 20px;
  color: #4B4B4B;
}
.join h2{
  font-size: 36px;
}
form input{
  margin: 20px 0;
  padding: 10px 20px;
  font-size: 24px;
  border-radius: 28px;
  border: 4px solid white;
}
footer{
  background: #F63232;
  color: white;
  padding: 10px;
  text-align: center;
}

/* pseudo classes */

nav li a:hover{
  text-decoration: underline;
}
.images li:hover{
  position: relative;
  top: -4px;
}
form input:focus{
  border: 4px dashed #4B4B4B;
  outline: none;
}
form input:valid{
  border: 4px solid #71D300;
}
/* 說明講解 :first-child */
/* nav li:first-child{
  border: 3px solid #F63232;
} */
article p::first-line{
  font-weight: bold;
  font-size: 1.2em;
}
section.join p::first-letter{
  font-size: 1.5em;
}
p::selection{
  background-color: #F63232;
  color: white;
}
p::after{
  content: '...';
}

/* responsive styles */

@media screen and (max-width: 1400px){

  .banner .welcome h2{
    font-size: 60px;
  }
  nav li{
    font-size: 18px;
  }

}

@media screen and (max-width: 960px){

  .banner .welcome h2{
    font-size: 40px;
  }

}

@media screen and (max-width: 700px){

  .banner .welcome{
    position: relative;
    text-align: center;
    padding: 10px;
  }
  .banner .welcome br{
    display: none;
  }
  .banner .welcome h2{
    font-size: 25px;
  }
  .banner .welcome span{
    font-size: 1em;
  }
  .images li{
    width: 100%;
    margin: 20px auto;
    display: block;
  }

}

@media screen and (max-width: 560px){

  nav li{
    font-size: 20px;
    display: block;
    width: 100%;
    margin: 12px 0;
  }
  header{
    position: relative;
  }
  nav{
    top: 0;
  }

}</code></pre>



<h2 class="wp-block-heading">#11 – Next Steps</h2>



<p>In this final video I’ll offer you some tips on what to study next, including other playlists from my YouTube channel, and my novice-to-ninja Udemy course about JavaScript:</p>



<ul class="wp-block-list"><li><a href="https://www.udemy.com/course/modern-javascript-from-novice-to-ninja/?couponCode=5B740E26BCA431311F66" target="_blank" rel="noreferrer noopener">Udemy Modern JavaScript Course (with discount applied)</a></li></ul>



<h4 class="wp-block-heading">其他課程清單</h4>



<ul class="wp-block-list"><li><a href="https://www.youtube.com/playlist?list=PL4cUxeGkcC9iGYgmEd2dm3zAKzyCGDtM5" target="_blank" rel="noreferrer noopener">CSS Animation playlist</a></li><li><a href="https://www.youtube.com/playlist?list=PL4cUxeGkcC9itC4TxYMzFCfveyutyPOCY" target="_blank" rel="noreferrer noopener">CSS Grid playlist</a></li><li><a href="https://www.youtube.com/playlist?list=PL4cUxeGkcC9i3FXJSUfmsNOx8E7u6UuhG" target="_blank" rel="noreferrer noopener">CSS Flexbox playlist</a></li><li><a href="https://www.youtube.com/watch?v=iWOYAxlnaww&amp;list=PL4cUxeGkcC9haFPT7J25Q9GRB_ZkFrQAc" target="_blank" rel="noreferrer noopener">Modern JavaScript preview</a></li><li><a href="https://www.youtube.com/playlist?list=PL4cUxeGkcC9g5_p_BVUGWykHfqx6bb7qK" target="_blank" rel="noreferrer noopener">Styling HTML Forms</a></li></ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Bootstrap 5 Crash Course Tutorial</title>
		<link>/wordpress_blog/bs5-crash-course-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Thu, 23 Dec 2021 08:36:00 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=448</guid>

					<description><![CDATA[學習資源：The Net Ninja – Youtube #1  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習資源：<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=O_9u1P5YjVc&amp;list=PL4cUxeGkcC9joIM91nLzd_qaH_AimmdAR" target="_blank">The Net Ninja – Youtube</a></p>



<h2 class="wp-block-heading">#1 – Intro &amp; Setup</h2>



<p>Learn how to create a responsive landing page with Bootstrap – using features &amp; componet new to Bootstrap 5 (accodion and offcanvas), in this Bootstrap 5 crash course tutorial series.</p>



<h3 class="wp-block-heading">What is Bootstrap (5)？</h3>



<ul class="wp-block-list"><li>Front-end, CSS &amp; JavaScript for making mobile-first, responsive websites</li><li>Includes pre-made components – navbars, modals, tabs, tooltips, buttons, accordions &amp; more…</li><li>Comes fully baked with a responsive, 12-column, CSS grid system for layouts</li><li>Easy to customize &amp; also ensures browser compatibility (for supported browsers)</li></ul>



<h4 class="wp-block-heading">使用工具</h4>



<ul class="wp-block-list"><li>Visual Studio Code<ul><li>套件1：Live Sass Compiler</li><li>套件2：Live Server</li></ul></li></ul>



<h4 class="wp-block-heading">使用 BS5 的方法有</h4>



<ul class="wp-block-list"><li>下載來源檔案</li><li>使用 CDN 載入</li><li>使用 npm 安裝 bootstrap</li></ul>



<h4 class="wp-block-heading">Access the course files on GitHub</h4>



<ul class="wp-block-list"><li><a href="https://github.com/iamshaunjp/bootstrap-5-tutorial" target="_blank" rel="noreferrer noopener">bootstrap-5-tutorial</a></li></ul>



<p>範例程式碼</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;h1&gt;Net Ninja Pro&lt;/h1&gt;

  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#2 – Bootstrap 5 New Features</h2>



<p>In this Bootstrap 5 tutorial you’ll learn about all the main new features since version 4 – including the dropping of jQuery (!!!), new utility classes, new components.</p>



<ul class="wp-block-list"><li>Bootstrap 5 has dropped(停止) jQuery!</li><li>Forms have been revamped(改造)</li><li>Right-To-Left (RTL) support added</li><li>New utility classes (positioning, font size, border radius etc)</li><li>Minor changes to some components &amp; grid (Jumbotron gone)</li><li>Bootstrap Icons</li><li>Two new components – Offcanvas &amp; Accordion</li></ul>



<h4 class="wp-block-heading">Customize → Color</h4>



<h2 class="wp-block-heading">#3 – Colours &amp; Typography</h2>



<p>In this Bootstrap 5 tutorial you’ll learn how to make use of the Bootstrap text &amp; headings styles as well as theme colours.</p>



<ul class="wp-block-list"><li>Reboot</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- heading tags --&gt;
  &lt;h1&gt;Bootstrap 5 Tutorial&lt;/h1&gt;
  &lt;h2&gt;this is an h2&lt;/h2&gt;
  &lt;h3&gt;this is an h3&lt;/h3&gt;
  &lt;h2 class="h3"&gt;this is an h2 tag with the h3 class&lt;/h2&gt;

  &lt;!-- display headings --&gt;
  &lt;h1 class="display-1"&gt;display 1 heading&lt;/h1&gt;
  &lt;h1 class="display-2"&gt;display 2 heading&lt;/h1&gt;
  &lt;h1 class="display-6"&gt;display 6 heading&lt;/h1&gt;
  &lt;p class="display-1"&gt;p tag with display-1 class&lt;/p&gt;

  &lt;!-- lead text &amp; alignment --&gt;
  &lt;small&gt;this is small text&lt;/small&gt;
  &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
  &lt;p class="lead"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
  &lt;p class="lead text-center"&gt;hello ninjas&lt;/p&gt;
  &lt;p class="lead text-end"&gt;hello ninjas&lt;/p&gt;
  &lt;p class="lead text-start"&gt;hello ninjas&lt;/p&gt;

  &lt;!-- text decoration &amp; font weight --&gt;
  &lt;p class="text-decoration-underline"&gt;this is underlined text&lt;/p&gt;
  &lt;p class="text-decoration-line-through"&gt;this is line through text&lt;/p&gt;
  &lt;p class="fw-bold"&gt;this is bold text&lt;/p&gt;
  &lt;small&gt;this is small text&lt;/small&gt;

  &lt;!-- text colours --&gt;
  &lt;p class="text-primary"&gt;theme primary colour&lt;/p&gt;
  &lt;p class="text-secondary"&gt;theme secondary colour&lt;/p&gt;
  &lt;p class="text-info"&gt;theme info colour&lt;/p&gt;
  &lt;p class="text-warning"&gt;theme warning colour&lt;/p&gt;
  &lt;p class="text-success"&gt;theme success colour&lt;/p&gt;
  &lt;p class="text-danger"&gt;theme danger colour&lt;/p&gt;
  &lt;p class="text-muted"&gt;theme muted colour&lt;/p&gt;
  &lt;!-- bg colors --&gt;
  &lt;p class="text-white bg-primary"&gt;white text on primary bg&lt;/p&gt;
  &lt;p class="text-white bg-secondary"&gt;white text on secondary bg&lt;/p&gt;
  &lt;p class="text-light bg-danger"&gt;white text on danger bg&lt;/p&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#4 – Buttons &amp; Button Groups</h2>



<p>In this Bootstrap 5 tutorial I’ll show you how to use the button classes and the button group component (to group buttons together).</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- basic buttons --&gt;
  &lt;h2&gt;Buttons&lt;/h2&gt;

  &lt;button class="btn btn-primary"&gt;primary button&lt;/button&gt;
  &lt;button class="btn btn-secondary"&gt;secondary button&lt;/button&gt;

  &lt;!-- anchor tags as buttons --&gt;
  &lt;h2&gt;Links as buttons&lt;/h2&gt;

  &lt;a href="#" class="btn btn-info"&gt;info anchor tag&lt;/a&gt;
  &lt;a href="#" class="btn btn-success"&gt;success anchor tag&lt;/a&gt;

  &lt;!-- button sizes --&gt;
  &lt;h2&gt;Button sizes&lt;/h2&gt;

  &lt;button class="btn btn-lg btn-danger"&gt;large danger button&lt;/button&gt;
  &lt;button class="btn btn-sm btn-warning"&gt;small warning button&lt;/button&gt;

  &lt;!-- outlined styles --&gt;
  &lt;h2&gt;Button Styles&lt;/h2&gt;

  &lt;button class="btn btn-outline-primary"&gt;outlined primary button&lt;/button&gt;
  &lt;button class="btn btn-outline-secondary btn-lg"&gt;large outlined secondary button&lt;/button&gt;

  &lt;!-- button groups --&gt;
  &lt;h2&gt;Button Groups&lt;/h2&gt;

  &lt;div class="btn-group"&gt;
    &lt;a href="#" class="btn btn-primary"&gt;button 1&lt;/a&gt;
    &lt;a href="#" class="btn btn-warning"&gt;button 2&lt;/a&gt;
    &lt;a href="#" class="btn btn-success"&gt;button 3&lt;/a&gt;
  &lt;/div&gt;

  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#5 – Utility Classes</h2>



<p>In this Bootstrap 5 tutorial I’ll show you some of the different utility classes – for things like spacing and border – that we can add to our HTML elements.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- margin and padding --&gt;
  &lt;div class="bg-primary m-1 p-1"&gt;small margin and padding&lt;/div&gt;
  &lt;div class="bg-primary m-5 p-5"&gt;large margin and padding&lt;/div&gt;
  &lt;div class="bg-primary my-3 px-5"&gt;margin in y direction, padding in x direction&lt;/div&gt;
  &lt;div class="bg-primary mt-3 mb-4 ms-5 me-1 ps-5 pt-4 pe-2 pb-1"&gt;m &amp; p for each direction&lt;/div&gt;

  &lt;!-- borders --&gt;
  &lt;div class="m-3 p-3 border"&gt;default border&lt;/div&gt;
  &lt;div class="m-3 p-3 border-top border-end"&gt;individual borders&lt;/div&gt;
  &lt;div class="m-3 p-3 border-start border-success"&gt;border success colour at start&lt;/div&gt;
  &lt;div class="m-3 p-3 border-start border-danger border-5"&gt;thicker border&lt;/div&gt;
  &lt;div class="m-3 p-3 rounded border border-5"&gt;rounded corners&lt;/div&gt;
  &lt;div class="m-3 p-3 rounded-pill border border-5"&gt;rounded pill corners&lt;/div&gt;

  &lt;!-- box shadow --&gt;
  &lt;div class="m-3 p-3 shadow-sm"&gt;element with small box shadow&lt;/div&gt;
  &lt;div class="m-3 p-3 shadow-lg"&gt;element with large box shadow&lt;/div&gt;

  &lt;!-- font weight --&gt;
  &lt;p class="fw-bold"&gt;bold text&lt;/p&gt;
  &lt;p class="fw-bolder"&gt;bolder text&lt;/p&gt;
  &lt;p&gt;normal text&lt;/p&gt;
  &lt;p class="fw-light"&gt;light text&lt;/p&gt;
  &lt;p class="fst-italic"&gt;italic text&lt;/p&gt;
  &lt;p class="fst-italic fw-light"&gt;italic light text&lt;/p&gt;

  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#6 – Containers</h2>



<p>Learn how to use containers in this Bootstrap 5 tutorial.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="container my-5"&gt;
    &lt;h2&gt;normal container&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui facilis maiores itaque? Tenetur corporis adipisci eaque, laudantium, reprehenderit numquam deserunt debitis eius nobis, blanditiis sequi ratione ipsum esse repudiandae quo.&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class="container-fluid"&gt;
    &lt;h2&gt;fluid container&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Laudantium consectetur dolorum pariatur laboriosam molestias delectus non fuga in magni dolor quibusdam magnam officia numquam, ullam eum aliquid ducimus repellendus neque!&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class="container-lg my-5"&gt;
    &lt;h2&gt;100% width until lg screens, then container&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio deleniti et temporibus, vero fugiat dolores, quaerat praesentium voluptatum dicta, soluta harum porro repellendus ipsa repudiandae sapiente est illo? Tenetur, voluptatibus?&lt;/p&gt;
  &lt;/div&gt;

  &lt;div class="container-xl my-5"&gt;
    &lt;h2&gt;100% width until xl screens, then container&lt;/h2&gt;
    &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae reprehenderit ex dolores sapiente architecto, odio, quam voluptates molestias, porro amet labore. Iusto aliquid voluptate magnam repudiandae ad totam nesciunt labore!&lt;/p&gt;
  &lt;/div&gt;

  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#7 – Grid Layout (part 1)</h2>



<p>Learn how to use the grid system in Bootstrap 5 – using containers, rows, columns and responsive classes too.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;div class="container-lg my-5"&gt;
    &lt;h2&gt;basic grid&lt;/h2&gt;
    &lt;div class="row"&gt;
      &lt;div class="col"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-1&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-2&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-3&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-4&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;div class="container-lg my-5"&gt;
    &lt;h2&gt;column widths&lt;/h2&gt;
    &lt;div class="row"&gt;
      &lt;div class="col-6"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-1&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-2&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-3&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;div class="container-lg my-5"&gt;
    &lt;h2&gt;responsive column widths&lt;/h2&gt;
    &lt;div class="row"&gt;
      &lt;div class="col-sm-4 col-lg-6"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-1&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-sm-4 col-lg-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-2&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-sm-4 col-lg-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col-3&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;div class="container-lg my-5"&gt;
    &lt;h2&gt;justifying columns&lt;/h2&gt;
    &lt;div class="row justify-content-center"&gt;
      &lt;div class="col-md-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col 1&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-md-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col 2&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="col-md-3"&gt;
        &lt;div class="p-5 bg-primary text-light"&gt;col 3&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">justifying columns</h4>



<ul class="wp-block-list"><li>justify-content-start</li><li>justify-content-center</li><li>justify-content-end</li><li>justify-content-around</li><li>justify-content-between</li><li>justify-content-evenly</li></ul>



<h2 class="wp-block-heading">#8 – Grid Layout (part 2)</h2>



<p>In this lesson we’ll take what we learnt about the grid system in Bootstrap 5 and use it to start our page design.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;


  &lt;!-- topics at a glance --&gt;


  &lt;!-- reviews list --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#9 – Navbars</h2>



<p>In this Bootstrap 5 tutorial you’ll learn how to use the navbar component and we’ll add it to our page design.</p>



<ul class="wp-block-list"><li>使用 Bootstrap 5 文件，搜尋元件找到想要的樣式、複製程式碼到網頁編輯器<ul><li>Containers</li><li>Brand</li><li>Nav</li><li>Color schemes</li><li>Placement</li><li>Toggler</li></ul></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;


  &lt;!-- topics at a glance --&gt;


  &lt;!-- reviews list --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#10 – Cards</h2>



<p>In this Bootstrap tutorial you’ll learn how to use the Card component – in our site we’ll use it for te pricing options.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;


  &lt;!-- reviews list --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#11 – Accordions</h2>



<p>In this Bootstrap tutorial we’ll look at one of the new components of Bootstrap 5 – the accordion component.</p>



<ul class="wp-block-list"><li>使用 Bootstrap 文件，了解程式碼範例的使用、調整，透過練習熟練功能的使用方式即可，不需要全部記憶。</li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;


  &lt;!-- contact form --&gt;


  &lt;!-- get updates / modal trigger --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#12 – List Groups</h2>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;


  &lt;!-- get updates / modal trigger --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#13 – Bootstrap Icons</h2>



<p>In this bootstrap tutorial we’ll take a look at the icon library and how to use icons in our web page.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;


  &lt;!-- get updates / modal trigger --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#14 – Working with Forms</h2>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;
  &lt;!-- form-control, form-label, form-select, input-group, input-group-text --&gt;
  &lt;section id="contact"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Get in Touch&lt;/h2&gt;
        &lt;p class="lead"&gt;Questions to ask? Fill out the form to contact me directly...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;form&gt;
            &lt;label for="email" class="form-label"&gt;Email address:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-envelope-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="email" class="form-control" id="email" placeholder="e.g. mario@example.ocm"&gt;
            &lt;/div&gt;
            
            &lt;label for="name" class="form-label"&gt;Name:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-person-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="text" class="form-control" id="name" placeholder="e.g. Mario"&gt;
            &lt;/div&gt;
            
            &lt;label for="subject" class="form-label"&gt;What is your question about?&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-chat-right-dots-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;select class="form-select" id="subject"&gt;
                &lt;option value="pricing" selected&gt;Pricing query&lt;/option&gt;
                &lt;option value="content"&gt;Content query&lt;/option&gt;
                &lt;option value="other"&gt;Other query&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;
            
            &lt;div class="form-floating mb-4 mt-5"&gt;
              &lt;textarea id="query" class="form-control" style="height: 140px"&gt;&lt;/textarea&gt;
              &lt;label for="query"&gt;Your query...&lt;/label&gt;
            &lt;/div&gt;

            &lt;div class="mb-4 text-center"&gt;
              &lt;button type="submit" class="btn btn-secondary"&gt;Submit&lt;/button&gt;
            &lt;/div&gt;

          &lt;/form&gt;
        &lt;/div&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- get updates / modal trigger --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#15 – Tooltips</h2>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;!-- tooltip --&gt;
          &lt;span class="tt" data-bs-placement="bottom" title="Net Ninja Book Cover"&gt;
            &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
          &lt;/span&gt;
          
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;
  &lt;!-- form-control, form-label, form-select, input-group, input-group-text --&gt;
  &lt;section id="contact"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Get in Touch&lt;/h2&gt;
        &lt;p class="lead"&gt;Questions to ask? Fill out the form to contact me directly...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;form&gt;
            &lt;label for="email" class="form-label"&gt;Email address:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-envelope-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="email" class="form-control" id="email" placeholder="e.g. mario@example.ocm"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Enter an email address we can reply to."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;

            &lt;/div&gt;
            
            &lt;label for="name" class="form-label"&gt;Name:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-person-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="text" class="form-control" id="name" placeholder="e.g. Mario"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Pretty self explanatory really..."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/div&gt;
            
            &lt;label for="subject" class="form-label"&gt;What is your question about?&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-chat-right-dots-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;select class="form-select" id="subject"&gt;
                &lt;option value="pricing" selected&gt;Pricing query&lt;/option&gt;
                &lt;option value="content"&gt;Content query&lt;/option&gt;
                &lt;option value="other"&gt;Other query&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;
            
            &lt;div class="form-floating mb-4 mt-5"&gt;
              &lt;textarea id="query" class="form-control" style="height: 140px"&gt;&lt;/textarea&gt;
              &lt;label for="query"&gt;Your query...&lt;/label&gt;
            &lt;/div&gt;

            &lt;div class="mb-4 text-center"&gt;
              &lt;button type="submit" class="btn btn-secondary"&gt;Submit&lt;/button&gt;
            &lt;/div&gt;

          &lt;/form&gt;
        &lt;/div&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- get updates / modal trigger --&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
  &lt;script&gt;
    const tooltips = document.querySelectorAll('.tt');
    tooltips.forEach(t =&gt; {
      new bootstrap.Tooltip(t);
    });
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#16 – Modals</h2>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;!-- tooltip --&gt;
          &lt;span class="tt" data-bs-placement="bottom" title="Net Ninja Book Cover"&gt;
            &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
          &lt;/span&gt;
          
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;
  &lt;!-- form-control, form-label, form-select, input-group, input-group-text --&gt;
  &lt;section id="contact"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Get in Touch&lt;/h2&gt;
        &lt;p class="lead"&gt;Questions to ask? Fill out the form to contact me directly...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;form&gt;
            &lt;label for="email" class="form-label"&gt;Email address:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-envelope-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="email" class="form-control" id="email" placeholder="e.g. mario@example.ocm"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Enter an email address we can reply to."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;

            &lt;/div&gt;
            
            &lt;label for="name" class="form-label"&gt;Name:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-person-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="text" class="form-control" id="name" placeholder="e.g. Mario"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Pretty self explanatory really..."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/div&gt;
            
            &lt;label for="subject" class="form-label"&gt;What is your question about?&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-chat-right-dots-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;select class="form-select" id="subject"&gt;
                &lt;option value="pricing" selected&gt;Pricing query&lt;/option&gt;
                &lt;option value="content"&gt;Content query&lt;/option&gt;
                &lt;option value="other"&gt;Other query&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;
            
            &lt;div class="form-floating mb-4 mt-5"&gt;
              &lt;textarea id="query" class="form-control" style="height: 140px"&gt;&lt;/textarea&gt;
              &lt;label for="query"&gt;Your query...&lt;/label&gt;
            &lt;/div&gt;

            &lt;div class="mb-4 text-center"&gt;
              &lt;button type="submit" class="btn btn-secondary"&gt;Submit&lt;/button&gt;
            &lt;/div&gt;

          &lt;/form&gt;
        &lt;/div&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- get updates / modal trigger --&gt;
  &lt;section class="bg-light"&gt;
    &lt;div class="container"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Stay in The Loop&lt;/h2&gt;
        &lt;p class="lead"&gt; Get the latest updates as they happen...&lt;/p&gt;
      &lt;/div&gt;
      &lt;div class="row justify-content-center"&gt;
        &lt;div class="col-md-8 text-center"&gt;
          &lt;p class="text-muted my-4"&gt;
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Suscipit officia odio, quo dolore quis debitis similique quod blanditiis eos voluptatum autem et dolorum exercitationem ratione nobis sint mollitia enim qui.
          &lt;/p&gt;
          &lt;button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#reg-modal"&gt;
            Register for Updates
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- modal itself --&gt;
  &lt;div class="modal fade" id="reg-modal" tabindex="-1" aria-labelledby="modal-title" aria-hidden="true"&gt;
    &lt;div class="modal-dialog"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h5 class="modal-title" id="modal-title"&gt;Get the Latest Updates&lt;/h5&gt;
          &lt;button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officiis, neque fugit odit velit quibusdam eligendi, commodi quam dolore, esse numquam iusto tenetur omnis ea cum sit rem doloribus ratione minus.&lt;/p&gt;
          &lt;label for="modal-email" class="form-label"&gt;Your email address:&lt;/label&gt;
          &lt;input type="email" class="form-control" id="modal-email" placeholder="e.g. mario@example.com"&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button class="btn btn-primary"&gt;Submit&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
  &lt;script&gt;
    const tooltips = document.querySelectorAll('.tt');
    tooltips.forEach(t =&gt; {
      new bootstrap.Tooltip(t);
    });
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#17 – Offcanvas</h2>



<p>In this Bootstrap5 tutorial you’ll learn about a new sidebar component – the Offcanvas.</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
          &lt;!-- open sidebar / offcanvas --&gt;
          &lt;a href="#sidebar" class="d-block mt-3" data-bs-toggle="offcanvas" role="button" aria-controls="sidebar"&gt;Explore my other books&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;!-- tooltip --&gt;
          &lt;span class="tt" data-bs-placement="bottom" title="Net Ninja Book Cover"&gt;
            &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
          &lt;/span&gt;
          
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;
  &lt;!-- form-control, form-label, form-select, input-group, input-group-text --&gt;
  &lt;section id="contact"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Get in Touch&lt;/h2&gt;
        &lt;p class="lead"&gt;Questions to ask? Fill out the form to contact me directly...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;form&gt;
            &lt;label for="email" class="form-label"&gt;Email address:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-envelope-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="email" class="form-control" id="email" placeholder="e.g. mario@example.ocm"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Enter an email address we can reply to."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;

            &lt;/div&gt;
            
            &lt;label for="name" class="form-label"&gt;Name:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-person-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="text" class="form-control" id="name" placeholder="e.g. Mario"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Pretty self explanatory really..."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/div&gt;
            
            &lt;label for="subject" class="form-label"&gt;What is your question about?&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-chat-right-dots-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;select class="form-select" id="subject"&gt;
                &lt;option value="pricing" selected&gt;Pricing query&lt;/option&gt;
                &lt;option value="content"&gt;Content query&lt;/option&gt;
                &lt;option value="other"&gt;Other query&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;
            
            &lt;div class="form-floating mb-4 mt-5"&gt;
              &lt;textarea id="query" class="form-control" style="height: 140px"&gt;&lt;/textarea&gt;
              &lt;label for="query"&gt;Your query...&lt;/label&gt;
            &lt;/div&gt;

            &lt;div class="mb-4 text-center"&gt;
              &lt;button type="submit" class="btn btn-secondary"&gt;Submit&lt;/button&gt;
            &lt;/div&gt;

          &lt;/form&gt;
        &lt;/div&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- get updates / modal trigger --&gt;
  &lt;section class="bg-light"&gt;
    &lt;div class="container"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Stay in The Loop&lt;/h2&gt;
        &lt;p class="lead"&gt; Get the latest updates as they happen...&lt;/p&gt;
      &lt;/div&gt;
      &lt;div class="row justify-content-center"&gt;
        &lt;div class="col-md-8 text-center"&gt;
          &lt;p class="text-muted my-4"&gt;
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Suscipit officia odio, quo dolore quis debitis similique quod blanditiis eos voluptatum autem et dolorum exercitationem ratione nobis sint mollitia enim qui.
          &lt;/p&gt;
          &lt;button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#reg-modal"&gt;
            Register for Updates
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- modal itself --&gt;
  &lt;div class="modal fade" id="reg-modal" tabindex="-1" aria-labelledby="modal-title" aria-hidden="true"&gt;
    &lt;div class="modal-dialog"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h5 class="modal-title" id="modal-title"&gt;Get the Latest Updates&lt;/h5&gt;
          &lt;button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officiis, neque fugit odit velit quibusdam eligendi, commodi quam dolore, esse numquam iusto tenetur omnis ea cum sit rem doloribus ratione minus.&lt;/p&gt;
          &lt;label for="modal-email" class="form-label"&gt;Your email address:&lt;/label&gt;
          &lt;input type="email" class="form-control" id="modal-email" placeholder="e.g. mario@example.com"&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button class="btn btn-primary"&gt;Submit&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- offcanvas --&gt;
  &lt;div class="offcanvas offcanvas-start" tabindex="-1" id="sidebar" aria-labelledby="sidebar-label"&gt;
    &lt;div class="offcanvas-header"&gt;
      &lt;h5 class="offcanvas-title" id="sidebar-label"&gt;My Other Books&lt;/h5&gt;
      &lt;button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"&gt;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class="offcanvas-body"&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae magnam sed fugiat. Earum incidunt provident voluptatibus praesentium eveniet rerum esse nostrum, inventore, nulla illo eius voluptas animi quidem veritatis iste!&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
  &lt;script&gt;
    const tooltips = document.querySelectorAll('.tt');
    tooltips.forEach(t =&gt; {
      new bootstrap.Tooltip(t);
    });
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">#18 – Dropdowns</h2>



<p>In this bootstrap tutorial I’ll show you how to use the dropdown component in our sidebar (offcanvas).</p>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
          &lt;!-- open sidebar / offcanvas --&gt;
          &lt;a href="#sidebar" class="d-block mt-3" data-bs-toggle="offcanvas" role="button" aria-controls="sidebar"&gt;Explore my other books&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;!-- tooltip --&gt;
          &lt;span class="tt" data-bs-placement="bottom" title="Net Ninja Book Cover"&gt;
            &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
          &lt;/span&gt;
          
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-light"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;
  &lt;!-- form-control, form-label, form-select, input-group, input-group-text --&gt;
  &lt;section id="contact"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Get in Touch&lt;/h2&gt;
        &lt;p class="lead"&gt;Questions to ask? Fill out the form to contact me directly...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;form&gt;
            &lt;label for="email" class="form-label"&gt;Email address:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-envelope-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="email" class="form-control" id="email" placeholder="e.g. mario@example.ocm"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Enter an email address we can reply to."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;

            &lt;/div&gt;
            
            &lt;label for="name" class="form-label"&gt;Name:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-person-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="text" class="form-control" id="name" placeholder="e.g. Mario"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Pretty self explanatory really..."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/div&gt;
            
            &lt;label for="subject" class="form-label"&gt;What is your question about?&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-chat-right-dots-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;select class="form-select" id="subject"&gt;
                &lt;option value="pricing" selected&gt;Pricing query&lt;/option&gt;
                &lt;option value="content"&gt;Content query&lt;/option&gt;
                &lt;option value="other"&gt;Other query&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;
            
            &lt;div class="form-floating mb-4 mt-5"&gt;
              &lt;textarea id="query" class="form-control" style="height: 140px"&gt;&lt;/textarea&gt;
              &lt;label for="query"&gt;Your query...&lt;/label&gt;
            &lt;/div&gt;

            &lt;div class="mb-4 text-center"&gt;
              &lt;button type="submit" class="btn btn-secondary"&gt;Submit&lt;/button&gt;
            &lt;/div&gt;

          &lt;/form&gt;
        &lt;/div&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- get updates / modal trigger --&gt;
  &lt;section class="bg-light"&gt;
    &lt;div class="container"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Stay in The Loop&lt;/h2&gt;
        &lt;p class="lead"&gt; Get the latest updates as they happen...&lt;/p&gt;
      &lt;/div&gt;
      &lt;div class="row justify-content-center"&gt;
        &lt;div class="col-md-8 text-center"&gt;
          &lt;p class="text-muted my-4"&gt;
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Suscipit officia odio, quo dolore quis debitis similique quod blanditiis eos voluptatum autem et dolorum exercitationem ratione nobis sint mollitia enim qui.
          &lt;/p&gt;
          &lt;button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#reg-modal"&gt;
            Register for Updates
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- modal itself --&gt;
  &lt;div class="modal fade" id="reg-modal" tabindex="-1" aria-labelledby="modal-title" aria-hidden="true"&gt;
    &lt;div class="modal-dialog"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h5 class="modal-title" id="modal-title"&gt;Get the Latest Updates&lt;/h5&gt;
          &lt;button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officiis, neque fugit odit velit quibusdam eligendi, commodi quam dolore, esse numquam iusto tenetur omnis ea cum sit rem doloribus ratione minus.&lt;/p&gt;
          &lt;label for="modal-email" class="form-label"&gt;Your email address:&lt;/label&gt;
          &lt;input type="email" class="form-control" id="modal-email" placeholder="e.g. mario@example.com"&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button class="btn btn-primary"&gt;Submit&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- offcanvas --&gt;
  &lt;div class="offcanvas offcanvas-start" tabindex="-1" id="sidebar" aria-labelledby="sidebar-label"&gt;
    &lt;div class="offcanvas-header"&gt;
      &lt;h5 class="offcanvas-title" id="sidebar-label"&gt;My Other Books&lt;/h5&gt;
      &lt;button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"&gt;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class="offcanvas-body"&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae magnam sed fugiat. Earum incidunt provident voluptatibus praesentium eveniet rerum esse nostrum, inventore, nulla illo eius voluptas animi quidem veritatis iste!&lt;/p&gt;
      &lt;!-- dropdown --&gt;
      &lt;div class="dropdown mt-3"&gt;
        &lt;button class="btn btn-secondary dropdown-toggle" type="button" id="book-dropdown" data-bs-toggle="dropdown"&gt;
          Choose a book title
        &lt;/button&gt;
        &lt;ul class="dropdown-menu" aria-labelledby="book-dropdown"&gt;
          &lt;li&gt;&lt;a href="#" class="dropdown-item"&gt;Become a React Superhero&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href="#" class="dropdown-item"&gt;Conquering Vue.js&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href="#" class="dropdown-item"&gt;Levelling up Your Next.js&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
  &lt;script&gt;
    const tooltips = document.querySelectorAll('.tt');
    tooltips.forEach(t =&gt; {
      new bootstrap.Tooltip(t);
    });
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="has-background wp-block-heading" style="background-color:#ff6663">#19 – Customizing Bootstrap</h2>



<p>in this Bootstrap tutorial I’ll show you how we can install Bootstrap locally and customize theme variables (like colors) using SASS.</p>



<ul class="wp-block-list"><li>SASS</li><li><a rel="noreferrer noopener" href="https://nodejs.org/en/" target="_blank">Node.js</a>&nbsp;– 使用 NPM 安裝 Bootstrap</li><li>Terminal(終端機)<ul><li>npm init，產生 package.json</li><li>npm install bootstrap，安裝 Bootstrap、在 package.json 的 dependencies 地方會顯示安裝好的 bootstrap名稱、版本</li></ul></li><li>node_modules/bootstrap/dist/css/bootstrap.min.css – 可以像 CDN 直接使用</li><li>創立一個新的資料夾以及檔案 sass/main.scss，而不是直接修改 bootstrap/scss/_variables.scss，防止變更到原始的 bootstrap 檔案</li><li>custom variables、import bootstrap</li><li>安裝 Live Sass Compiler</li><li>打開 Settings 搜尋 live sass format、點擊 Live Sass Compile，Settings:Formats 編輯新增以下程式碼</li><li>新增 custom-theme-colors</li><li>使用 map-merge 合併原本和新增的變數顏色<ul><li>5.0.x 與 5.1.x 有所更動</li></ul></li></ul>



<pre class="wp-block-code"><code>// index.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en zh-hant"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;!-- &lt;link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"&gt; --&gt;
  &lt;link rel="stylesheet" href="css/main.min.css"&gt;
  &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"&gt;
  &lt;title&gt;Net Ninja Pro - the Book&lt;/title&gt;
  &lt;style&gt;
    section {
      padding: 60px 0;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- navbar --&gt;
  &lt;nav class="navbar navbar-expand-md navbar-light"&gt;
    &lt;div class="container-xxl"&gt;
      &lt;a href="#intro" class="navbar-brand"&gt;
        &lt;span class="fw-bold text-secondary"&gt;
          &lt;i class="bi bi-book-half"&gt;&lt;/i&gt;
          Net Ninja Pro - the Book
        &lt;/span&gt;
      &lt;/a&gt;
      &lt;!-- toggle button for mobile nav --&gt;
      &lt;button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-nav" aria-controls="main-nav" aria-expanded="false" aria-label="Toggle navigation"&gt;
        &lt;span class="navbar-toggler-icon"&gt;&lt;/span&gt;
      &lt;/button&gt;
      &lt;!-- navbar links --&gt;
      &lt;div class="collapse navbar-collapse justify-content-end align-center" id="main-nav"&gt;
        &lt;ul class="navbar-nav"&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#topics"&gt;About The Book&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#reviews"&gt;Reviews&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item"&gt;
            &lt;a class="nav-link" href="#contact"&gt;Get in Touch&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item d-md-none"&gt;
            &lt;a class="nav-link" href="#pricing"&gt;Pricing&lt;/a&gt;
          &lt;/li&gt;
          &lt;li class="nav-item ms-2 d-none d-md-inline"&gt;
            &lt;a class="btn btn-secondary" href="#pricing"&gt;buy now&lt;/a&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;

  &lt;!-- main image &amp; intro text --&gt;
  &lt;section id="intro"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="row justify-content-center align-items-center"&gt;
        &lt;div class="col-md-5 text-center text-md-start"&gt;
          &lt;h1&gt;
            &lt;div class="display-2"&gt;Black-Belt&lt;/div&gt;
            &lt;div class="display-5 text-muted"&gt;Your Coding Skills&lt;/div&gt;
          &lt;/h1&gt;
          &lt;p class="lead my-4 text-muted"&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit.&lt;/p&gt;
          &lt;a href="#pricing" class="btn btn-secondary btn-lg"&gt;Buy Now&lt;/a&gt;
          &lt;!-- open sidebar / offcanvas --&gt;
          &lt;a href="#sidebar" class="d-block mt-3" data-bs-toggle="offcanvas" role="button" aria-controls="sidebar"&gt;Explore my other books&lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="col-md-5 text-center d-none d-md-block"&gt;
          &lt;!-- tooltip --&gt;
          &lt;span class="tt" data-bs-placement="bottom" title="Net Ninja Book Cover"&gt;
            &lt;img class="img-fluid" src="/assets/ebook-cover.png" alt="ebook cover"&gt;
          &lt;/span&gt;
          
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  
  &lt;!-- pricing plans --&gt;
  &lt;section id="pricing" class="bg-light mt-5"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Pricing Plans&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;Choose a pricing plan to suit you.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 align-items-center justify-content-center g-0"&gt;
        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Starter Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download only&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$12.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-9 col-lg-4"&gt;
          &lt;div class="card border-primary border-2"&gt;
            &lt;div class="card-header text-center text-primary"&gt;Most Popular&lt;/div&gt;
            &lt;div class="card-body text-center py-5"&gt;
              &lt;h4 class="card-title"&gt;Complete Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;eBook download &amp; all updates&lt;/p&gt;
              &lt;p class="display-4 my-4 text-primary fw-bold"&gt;$18.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-8 col-lg-4 col-xl-3"&gt;
          &lt;div class="card border-0"&gt;
            &lt;div class="card-body text-center py-4"&gt;
              &lt;h4 class="card-title"&gt;Ultimate Edition&lt;/h4&gt;
              &lt;p class="lead card-subtitle"&gt;download, updates &amp; extras&lt;/p&gt;
              &lt;p class="display-5 my-4 text-primary fw-bold"&gt;$24.99&lt;/p&gt;
              &lt;p class="card-text mx-5 text-muted d-none d-lg-block"&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit.&lt;/p&gt;
              &lt;a href="#" class="btn btn-outline-primary btn-lg mt-3"&gt;Buy Now&lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

      &lt;/div&gt;

      

      
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- topics at a glance --&gt;
  &lt;section id="topics"&gt;
    &lt;div class="container-md"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Inside the Book...&lt;/h2&gt;
        &lt;p class="lead text-muted"&gt;A quick glance at the topics you'll learn&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row my-5 g-5 justify-content-around align-items-center"&gt;
        &lt;div class="col-6 col-lg-4"&gt;
          &lt;img src="/assets/kindle.png" class="img-fluid" alt="ebook"&gt;
        &lt;/div&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;!-- accordion --&gt;
          &lt;div class="accordion" id="chapters"&gt;
            
            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-1"&gt;
                &lt;button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-1" aria-expanded="true" aria-controls="chapter-1"&gt;
                  Chapter 1 - Your First Web Page
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-1" class="accordion-collapse collapse show" aria-labelledby="heading-1" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-2"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-2" aria-expanded="false" aria-controls="chapter-2"&gt;
                  Chapter 2 - Mastering CSS
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-2" class="accordion-collapse collapse" aria-labelledby="heading-2" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-3"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-3" aria-expanded="false" aria-controls="chapter-3"&gt;
                  Chapter 3 - The Power of JavaScript
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-3" class="accordion-collapse collapse" aria-labelledby="heading-3" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-4"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-4" aria-expanded="false" aria-controls="chapter-4"&gt;
                  Chapter 4 Storing Data (Firebase Databases)
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-4" class="accordion-collapse collapse" aria-labelledby="heading-4" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="accordion-item"&gt;
              &lt;h2 class="accordion-header" id="heading-5"&gt;
                &lt;button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#chapter-5" aria-expanded="false" aria-controls="chapter-5"&gt;
                  Chapter 5 - User Authentication
                &lt;/button&gt;
              &lt;/h2&gt;
              &lt;div id="chapter-5" class="accordion-collapse collapse" aria-labelledby="heading-5" data-bs-parent="#chapters"&gt;
                &lt;div class="accordion-body"&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur, adipisicing elit. Fugit veritatis ipsam eaque officiis quaerat! Eligendi laborum cupiditate sed corporis animi voluptatem adipisci, ex est ducimus facilis commodi. A, atque fuga?&lt;/p&gt;
                  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam nostrum aliquam, aut iure optio modi assumenda consequuntur recusandae minima possimus aspernatur obcaecati incidunt necessitatibus. Ea corporis rerum veniam! Perspiciatis, aperiam!&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- reviews list --&gt;
  &lt;section id="reviews" class="bg-altlight"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;&lt;i class="bi bi-stars text-altdark"&gt;&lt;/i&gt;Book Reviews&lt;/h2&gt;
        &lt;p class="lead"&gt;What my students have said about the book...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;
        &lt;div class="col-lg-8"&gt;
          &lt;div class="list-group"&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill text-altdark"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill text-altdark"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill text-altdark"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill text-altdark"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill text-altdark"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
            &lt;div class="list-group-item py-3"&gt;
              &lt;div class="pb-2"&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-fill"&gt;&lt;/i&gt;
                &lt;i class="bi bi-star-half"&gt;&lt;/i&gt;
              &lt;/div&gt;
              &lt;h5 class="mb-1"&gt;A must buy for every aspiring web dev&lt;/h5&gt;
              &lt;p class="mb-1"&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse harum perspiciatis ab commodi quis totam omnis voluptatum, deleniti iure aliquid obcaecati dignissimos earum neque velit itaque eos accusantium expedita. Placeat.&lt;/p&gt;
              &lt;small&gt;Review by Mario&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- contact form --&gt;
  &lt;!-- form-control, form-label, form-select, input-group, input-group-text --&gt;
  &lt;section id="contact"&gt;
    &lt;div class="container-lg"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Get in Touch&lt;/h2&gt;
        &lt;p class="lead"&gt;Questions to ask? Fill out the form to contact me directly...&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="row justify-content-center my-5"&gt;

        &lt;div class="col-lg-6"&gt;
          &lt;form&gt;
            &lt;label for="email" class="form-label"&gt;Email address:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-envelope-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="email" class="form-control" id="email" placeholder="e.g. mario@example.ocm"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Enter an email address we can reply to."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;

            &lt;/div&gt;
            
            &lt;label for="name" class="form-label"&gt;Name:&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-person-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;input type="text" class="form-control" id="name" placeholder="e.g. Mario"&gt;
              &lt;!-- tooltip --&gt;
              &lt;span class="input-group-text"&gt;
                &lt;span class="tt" data-bs-placement="bottom" title="Pretty self explanatory really..."&gt;
                  &lt;i class="bi bi-question-circle text-muted"&gt;&lt;/i&gt;
                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/div&gt;
            
            &lt;label for="subject" class="form-label"&gt;What is your question about?&lt;/label&gt;
            &lt;div class="input-group mb-4"&gt;
              &lt;span class="input-group-text"&gt;
                &lt;i class="bi bi-chat-right-dots-fill"&gt;&lt;/i&gt;
              &lt;/span&gt;
              &lt;select class="form-select" id="subject"&gt;
                &lt;option value="pricing" selected&gt;Pricing query&lt;/option&gt;
                &lt;option value="content"&gt;Content query&lt;/option&gt;
                &lt;option value="other"&gt;Other query&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;
            
            &lt;div class="form-floating mb-4 mt-5"&gt;
              &lt;textarea id="query" class="form-control" style="height: 140px"&gt;&lt;/textarea&gt;
              &lt;label for="query"&gt;Your query...&lt;/label&gt;
            &lt;/div&gt;

            &lt;div class="mb-4 text-center"&gt;
              &lt;button type="submit" class="btn btn-secondary"&gt;Submit&lt;/button&gt;
            &lt;/div&gt;

          &lt;/form&gt;
        &lt;/div&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- get updates / modal trigger --&gt;
  &lt;section class="bg-light"&gt;
    &lt;div class="container"&gt;
      &lt;div class="text-center"&gt;
        &lt;h2&gt;Stay in The Loop&lt;/h2&gt;
        &lt;p class="lead"&gt; Get the latest updates as they happen...&lt;/p&gt;
      &lt;/div&gt;
      &lt;div class="row justify-content-center"&gt;
        &lt;div class="col-md-8 text-center"&gt;
          &lt;p class="text-muted my-4"&gt;
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Suscipit officia odio, quo dolore quis debitis similique quod blanditiis eos voluptatum autem et dolorum exercitationem ratione nobis sint mollitia enim qui.
          &lt;/p&gt;
          &lt;button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#reg-modal"&gt;
            Register for Updates
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;


  &lt;!-- modal itself --&gt;
  &lt;div class="modal fade" id="reg-modal" tabindex="-1" aria-labelledby="modal-title" aria-hidden="true"&gt;
    &lt;div class="modal-dialog"&gt;
      &lt;div class="modal-content"&gt;
        &lt;div class="modal-header"&gt;
          &lt;h5 class="modal-title" id="modal-title"&gt;Get the Latest Updates&lt;/h5&gt;
          &lt;button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="modal-body"&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officiis, neque fugit odit velit quibusdam eligendi, commodi quam dolore, esse numquam iusto tenetur omnis ea cum sit rem doloribus ratione minus.&lt;/p&gt;
          &lt;label for="modal-email" class="form-label"&gt;Your email address:&lt;/label&gt;
          &lt;input type="email" class="form-control" id="modal-email" placeholder="e.g. mario@example.com"&gt;
        &lt;/div&gt;
        &lt;div class="modal-footer"&gt;
          &lt;button class="btn btn-primary"&gt;Submit&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- offcanvas --&gt;
  &lt;div class="offcanvas offcanvas-start" tabindex="-1" id="sidebar" aria-labelledby="sidebar-label"&gt;
    &lt;div class="offcanvas-header"&gt;
      &lt;h5 class="offcanvas-title" id="sidebar-label"&gt;My Other Books&lt;/h5&gt;
      &lt;button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"&gt;&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class="offcanvas-body"&gt;
      &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae magnam sed fugiat. Earum incidunt provident voluptatibus praesentium eveniet rerum esse nostrum, inventore, nulla illo eius voluptas animi quidem veritatis iste!&lt;/p&gt;
      &lt;!-- dropdown --&gt;
      &lt;div class="dropdown mt-3"&gt;
        &lt;button class="btn btn-secondary dropdown-toggle" type="button" id="book-dropdown" data-bs-toggle="dropdown"&gt;
          Choose a book title
        &lt;/button&gt;
        &lt;ul class="dropdown-menu" aria-labelledby="book-dropdown"&gt;
          &lt;li&gt;&lt;a href="#" class="dropdown-item"&gt;Become a React Superhero&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href="#" class="dropdown-item"&gt;Conquering Vue.js&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href="#" class="dropdown-item"&gt;Levelling up Your Next.js&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;


  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
  &lt;script&gt;
    const tooltips = document.querySelectorAll('.tt');
    tooltips.forEach(t =&gt; {
      new bootstrap.Tooltip(t);
    });
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<pre class="wp-block-code"><code>// sass/main.scss

// custom variables
$primary: #c29938;

// import bootstrap
@import '../node_modules/bootstrap/scss/bootstrap';
</code></pre>



<pre class="wp-block-code"><code>// Live Sass Compile,Setting: Formats

"liveSassCompile.settings.formats": &#91;
        {
            "format": "compressed",
            "extensionName": ".min.css",
            "savePath": "/css"
        }
        // {
        //     "format": "expanded",
        //     "extensionName": ".css",
        //     "savePath": null
        // }
    ]</code></pre>



<pre class="wp-block-code"><code>// sass/main.scss - 新增 custom-theme-colors 並合併到 theme-colors

// custom variables
$primary: #c29938;
$light: #fbf5e5;

// import the functions &amp; variables
@import "../node_modules/bootstrap/scss/functions";
@import "../node_modules/bootstrap/scss/variables";

$custom-theme-colors: (
  "altlight": #f2ebfa,
  "altdark": #522192
);

$theme-colors: map-merge($theme-colors, $custom-theme-colors);

// bootstrap 5.1.x version
$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value");
$utilities-colors: map-merge($utilities-colors, $theme-colors-rgb);
$utilities-text-colors: map-loop($utilities-colors, rgba-css-var, "$key", "text");
$utilities-bg-colors: map-loop($utilities-colors, rgba-css-var, "$key", "bg");

// import bootstrap
@import '../node_modules/bootstrap/scss/bootstrap';
</code></pre>



<h2 class="wp-block-heading">#20 – Tabs</h2>



<p>In this final Bootstrap 5 tutorial I’ll show you how to use the tabs component.</p>



<pre class="wp-block-code"><code>// tabs.html

&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;link rel="stylesheet" href="/css/main.min.css"&gt;
  &lt;title&gt;Tabs&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  
  &lt;!-- tabs --&gt;
  &lt;div class="container my-5"&gt;
    &lt;nav&gt;
      &lt;div class="nav nav-tabs" id="nav-tab" role="tablist"&gt;
        &lt;button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true"&gt;Home&lt;/button&gt;
        &lt;button class="nav-link" id="nav-profile-tab" data-bs-toggle="tab" data-bs-target="#nav-profile" type="button" role="tab" aria-controls="nav-profile" aria-selected="false"&gt;Profile&lt;/button&gt;
        &lt;button class="nav-link" id="nav-contact-tab" data-bs-toggle="tab" data-bs-target="#nav-contact" type="button" role="tab" aria-controls="nav-contact" aria-selected="false"&gt;Contact&lt;/button&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
    &lt;div class="tab-content" id="nav-tabContent"&gt;

      &lt;div class="tab-pane fade show active p-3" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab"&gt;
        &lt;h2&gt;Home&lt;/h2&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequuntur laboriosam corporis harum autem! Veritatis expedita delectus voluptatibus tenetur, quis, ab ducimus nisi voluptatum a libero omnis id, aut quam quasi!&lt;/p&gt;
        &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Autem suscipit soluta rerum quae, nemo, cumque neque amet excepturi vero a ut facere voluptate nam doloribus? Saepe libero nesciunt delectus eius?&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="tab-pane fade p-3" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab"&gt;
        &lt;h2&gt;Profile&lt;/h2&gt;
        &lt;p&gt;Lorem ipsum dolor sit, amet consectetur adipisicing elit. Nemo eos dolorem eaque mollitia veniam eius delectus voluptatibus ut maxime assumenda, consequuntur dolores nostrum rerum a ab esse dolore et optio?&lt;/p&gt;
        &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero delectus veniam est harum eos consectetur, enim beatae culpa assumenda quo officiis numquam explicabo aperiam dolore, debitis in asperiores odit! Excepturi.&lt;/p&gt;
      &lt;/div&gt;

      &lt;div class="tab-pane fade p-3" id="nav-contact" role="tabpanel" aria-labelledby="nav-contact-tab"&gt;
        &lt;h2&gt;Contact&lt;/h2&gt;
        &lt;p&gt;Lorem ipsum dolor, sit amet consectetur adipisicing elit. Repellendus reiciendis pariatur sit quasi quas, odio libero optio amet atque ab? Non voluptatum sint perferendis in facere repellat aut vero sed.&lt;/p&gt;
        &lt;p&gt;Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ad quo mollitia minus nesciunt asperiores voluptate minima eaque, sed rerum, quasi dicta suscipit. Aspernatur fuga placeat neque consectetur, iusto corporis ex.&lt;/p&gt;
      &lt;/div&gt;

    &lt;/div&gt;
  &lt;/div&gt;



  &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
