---
# System prepended metadata

title: '小專案二: Movie Seat Booking | 電影訂票系統'
tags: [JS小專案]

---

# 小專案二: Movie Seat Booking | 電影訂票系統
###### tags: `JS小專案`

## 成品示意圖
![](https://i.imgur.com/GVA8kvb.png)

## HTML的結構
### 基礎設定
首先是基礎的設定，將**網頁標題**、**CSS**與**JS**的連結都設定好

```htmlembedded=
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 開頭設定CSS連結路徑 -->
  <link rel="stylesheet" href="style.css">
  <!-- 設定網頁標題 -->
  <title>Movie Seat Booking</title>
</head>
<body>
```
### 電影選單
接著咱們一動一動來，首先創建一個div，其class就取名為"movie-container"
使用**label**與**select**的標籤來創建下拉式列表
```htmlembedded=
  <!-- 設計一個container，主要是針對最上方的選單列表 -->
  <div class="movie-container">
    <label>Pick a movie:</label>
    <select id="movie">
      <!-- 設計四個選項，主要是撰寫票價選擇 -->
      <option value="10">Avengers: Endgame ($10)</option>
      <option value="12">Joker ($12)</option>
      <option value="8">Toy Story 4 ($8)</option>
      <option value="9">The Lion King ($9)</option>
    </select>
  </div>
```
### 座位範例
創建一個**unorder list**，列表(**li**)包含座位的三種狀態與說明文字
```htmlembedded=
  <!-- 展示座位的三種型態 -->
  <ul class="showcase">
    <li>
      <div class="seat"></div>
        <small>N/A</small>
    </li>
    <li>
      <div class="seat selected"></div>
        <small>Selected</small>  
    </li>
    <li>
      <div class="seat occupied"></div>
        <small>Occupied</small>
    </li>
  </ul>
 ```
### 螢幕與座位排列
建立一個div命名為container，其中包含螢幕(screen)與座位(row)
座位為一列8個，共計6排，針對幾個座位我們隨機命名為佔位的狀態(seat occupied)
```htmlembedded=
  <!-- 把螢幕與座位規劃在一區 -->
  <div class="container">
    <div class="screen"></div>
    <!-- 一排座位8個，共計規劃六排 -->
    <div class="row">
      <!-- 預計先有幾個座位被占 -->
      <div class="seat occupied"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat occupied"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
    </div>
    <div class="row">
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat"></div>
    </div>
    <div class="row">
      <div class="seat"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
    </div>
    <div class="row">
      <div class="seat occupied"></div>
      <div class="seat occupied"></div>
      <div class="seat occupied"></div>
      <div class="seat occupied"></div>
      <div class="seat occupied"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
    </div>
    <div class="row">
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
    </div>
    <div class="row">
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
      <div class="seat"></div>
    </div>
  </div>
  ```
### 票價計算
我們透過使用標籤**p**來建立文字，為了能讓特定的數字方便後面程式碼來計算，使用**span**座位數與票價給框起來

喔對了，JS的引用在最後面
```htmlembedded=
  <p class="text">You have selected <span id="count">0</span> seats for a price of $<span id="total">0</span></p>
  <!-- 設定js檔案路徑 -->
  <script src="script.js"></script>
</body>
</html>
```

## CSS的結構
### 字型設定
首先，咱們先用import的方式設定字型為Lato
```css=
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
```

### 邊框設定
當我們使用元素樣式為 box-sizing: border-box;，這個元素的內距和邊框將不會增加元素本身的寬度。
[參考資料](https://zh-tw.learnlayout.com/box-sizing.html)

```css=
*{
  box-sizing: border-box;
}
```

示意圖: 
![](https://i.imgur.com/Eoiby2I.png)

### 整體排列
為了讓整體背景顏色、字體一致，排列置中，採用**display: flex**，讓物件於中央集中

```css=
body {
  background-color: #242333;
  color: #fff;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  font-family: 'Lato', sans-serif;
  margin: 0;
}
```


### 下拉式選單
用margin的方式讓選單與上下各保持一點距離

```css=
/* 電影選擇區 */
.movie-container {
 margin: 20px 0; 
}

/* 電影選項欄位的美工 */
.movie-container select{
  background-color: #fff;
  border: 0px;
  border-radius: 5px;
  font-size: 14px;
  margin-left: 10px;
  padding: 5px 15px 5px 15px;
  -moz-appearance: none;
  -webkit-appearance: none;
  appearance: none;
}
```

### 座位區設計
主要掌握幾個關鍵點:
1. 先匡出一個區域來包含螢幕跟座位
2. 座位的美工與原角的修飾
3. 使用`nth-of-type()`、`nth-last-of-type()`來排列座位

```css=
/* 螢幕與座位區的整體配置 */
.container {
  perspective: 1000px;
  margin-bottom: 30px;
}

/* 座位的美工 */
.seat {
  background-color: #444451;
  height: 12px;
  width: 15px;
  margin: 3px;
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
}

/* 每排有八個座位 */

.seat.selected {
  background-color: #6feaf6;

}

.seat.occupied {
  background-color: #fff;
}

/* 選取第n個元素 */
.seat:nth-of-type(2){
  margin-right: 18px;
}
/* 選取最後數來第n個元素 */
.seat:nth-last-of-type(2){
  margin-left: 18px;
}
```
### 座位區動畫
使用`hover`功能，分別針對已經被占走和空缺的位子進行動畫設計
```css=
/* 使用hover的功能，讓游標指到可選取的座位時將會放大1.2倍 */
.seat:not(.occupied):hover{
  cursor: pointer;
  transform: scale(1.2);
}

/* 讓showcase(範例)的圖示呈現靜止狀態 */
.showcase .seat:not(.occupied):hover{
  cursor: default;
  transform: scale(1);
}
```
### 座位狀態範例設計
主要針對圖案與文字的排列是否整齊
```css=
/* 範例區塊的文字與配置 */
.showcase {
  background: rgba(0,0,0,0.1);
  padding: 5px 10px;
  border-radius: 5px;
  color: #777;
  list-style-type: none;
  display: flex;
  justify-content: space-between;
}

/* 排列範例內的物件 */
/* 使用flex讓物件並排 */
.showcase li{
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 10px;
}

/* 讓範例內的文字跟圖示有些區隔 */
.showcase li small {
  margin-left: 2px;
}



/* 使用flex讓每列座位整齊排列 */
.row {
  display: flex;
}
```

### 電影螢幕
```css=
/* 電影螢幕的設計 */
.screen {
  background-color: #fff;
  height: 70px;
  width: 100%;
  margin: 15px 0;
  transform: rotateX(-45deg);
  box-shadow: 0 3px 10px rgba(255, 255, 255, 0.7);
}
```
### 票價顯示文字
```css=
p.text {
  margin: 5px 0;
}

p.text span{
 color: #6feaf6;
}
```

## Javascript語言架構
### 針對需要回傳資料的部分進行設定
由於JS語言就是要讓網頁可以被互動，所以針對特定區塊或物件，使用`querySelector`、`querySelectorAll`與`getElementByID`來設定

這邊不要輕易地使用字面上的意思來猜測可能的功能，每個方法都有特定的回傳手法，例如只回傳一個值，或是回傳全部的元素

```javascript=
// 抓取container的資料
const container = document.querySelector('.container');
// querySelctor: Returns the first element that is a descendant of node that matches selectors.

const seats = document.querySelectorAll('.row .seat:not(.occupied)');
// querySelectorAll: Returns all element descendants of node that match selectors.

const count = document.getElementById('count');
// getElementById: Returns a reference to the first object with the specified value of the ID attribute.

const total = document.getElementById('total');

const movieSelect = document.getElementById('movie');
```

### 回傳票價、座位
這裡有幾個重點
1. 讓票價可以將選擇的項目轉換成值，並且將計算結果顯示在頁面上
2. 讓座位可以被點選或取消
3. 票價、電影選擇與座位的結果全部都能被本地端所儲存

```javascript=
populateUI();

// const ticketPrice = parseInt(movieSelect.Value);
let ticketPrice = parseInt(movieSelect.value);

// Save Selected movie index and price
function setMovieData(movieIndex, moviePrice){
  localStorage.setItem('selectedMovieIndex',movieIndex);
  localStorage.setItem('selectMoviePrice',moviePrice);
}

//Update total and Count
function updateSelectedCount() {
  //注意: .row .seat.selected 不等於 .row.seat.selected
  // .row .seat.selected 代表<div>row下面的class "seat selected"
  const selectedSeats = document.querySelectorAll('.row .seat.selected');
    // Copy selected seats into arr
    // Map through aray
    // Return a new array indexes
    // 使用...可以把多個目標內的內容依次選取完成
    // 使用map可以用來傳遞陣列
    // 被選取的座位會回傳以陣列所屬的index
    const seatsIndex = [...selectedSeats].map(seat => [...seats].indexOf(seat));

    localStorage.setItem('selectedSeats', JSON.stringify(seatsIndex));



    console.log(seatsIndex);

  //使用Length回傳所選取的座位數量
  //Returns the number of nodes in the collection.
  const selectedSeatsCount = selectedSeats.length;
  
  // 把選取的座位數跟票價進行運算，反應回頁面數字
  count.innerText = selectedSeatsCount;
  total.innerText = selectedSeatsCount * ticketPrice;
}

// Get data from localstorage and populate UI

function populateUI() {
  //把選取的位置紀錄儲存在本地端的記憶體
  const selectedSeats = JSON.parse(localStorage.getItem('selectedSeats')
  );
  // 條件一: 只要選取的seat不為null且選取的座位數量大於0
  // 條件二: index>-1下，把seat的classlist新增selected
  // indexOf下，如值為-1，則代表無回傳任何東西
  if(selectedSeats !== null && selectedSeats.length > 0) {
    seats.forEach((seat, index) => {
      if(selectedSeats.indexOf(index) > -1){
        seat.classList.add('selected');
      }
    });
  }
  // 把選取的電影紀錄儲存在本地端當中
  const selectedMovieIndex = localStorage.getItem('selectedMovieIndex');
  if (selectedMovieIndex !== null) {
    movieSelect.selectedIndex = selectedMovieIndex;
  }
}

// Movie Select event
movieSelect.addEventListener('change', e => {
  ticketPrice = +e.target.value;
  setMovieData(e.target.selectedIndex, e.target.value);
  updateSelectedCount();
});



// Seat Click Event
// 使用addEventListener來追蹤每個點選動作
// 目標為container
container.addEventListener('click', e =>{
  // Target內有多個Class，所以使用e.target.classList.contains的方法來確認是否有相符的字串
  // 欲探討主題: e.target.classList與contains放在一起的用法
  if(e.target.classList.contains('seat') && !e.target.classList.contains('occupied')){
    console.log(e.target);
  }{
    // 使用toggle把座位變成按鈕，把target property變成該id的class特性
    // 疑點: 為什麼這個會作用?
    e.target.classList.toggle('selected');
    updateSelectedCount();
  }
})

// Initial count and total set
// 讓一開始就顯示正確計算的票價
updateSelectedCount();