# 小專案二: Movie Seat Booking | 電影訂票系統
###### tags: `JS小專案`
## 成品示意圖

## 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;
}
```
示意圖:

### 整體排列
為了讓整體背景顏色、字體一致,排列置中,採用**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();