# Appworks駐點筆記
## 前端精髓: f(state) = UI (f 是 component)
* flotter雙平台app概念
React強項:管理資料的工具類軟體
React是單向資料流,以prop來舉例,只能從上往下丟,優點是單純
Vue/Anguler是雙向,可以從下往上丟,還能雙向綁定等等
## 週六
考試:早九到晚六
先處理firebase/github
## 周一
一對一公佈成績
做週二簡報
## 周二
隔天分享預演 3分鐘
舒壓workshop
## 週三
主題分享:
wireframe
競品分析
技術研究
## 小套件 / 快捷鍵
* [Electron](https://www.electronjs.org/) 桌面應用程式
* [FE dashboard](https://www.fetoolkit.io/) 前端常用小工具集
* [use-Immer](https://github.com/immerjs/use-immer#useimmerreducer) 小套件,幫你deepcopy的hook
* `command + shift + .` 可以看到mac的隱藏檔案
* [Socket.io](https://socket.io/): WebSocket 的 librar,業界常用
* [web RTC]() | [PEERJS](): 視訊用套件
* [Message schaduler](https://discord.bots.gg/bots/810918366045798451) discord機器人,提醒行事曆
* [starbugs](https://weekly.starbugs.dev/):台灣技術週刊
* [frontend weekly](https://frontendweekly.co/):前端技術週刊
* [webtools weekly](https://webtoolsweekly.com/):工具技術週刊
* [DevDocs API](https://devdocs.io/) | [MDN](https://developer.mozilla.org/zh-TW/):好用的工程師文件
* [QuickRef.ME](https://quickref.me/):常用指令作弊清單
* [logseq](https://logseq.com/):筆記軟體
* 隨便製造內文 [Lorem Ipsum](https://www.lipsum.com/)
#### npm套件
* [phosphoricons](https://phosphoricons.com/) | [heroicons](https://heroicons.com/): 可愛好用icon
* [Lodash](https://lodash.com/): 強大的JS函式庫,提供很多好用的method
* [i18next](https://www.i18next.com/) | react-i18next | i18next-icu | intl-messageformat | [fireStore插件](https://extensions.dev/extensions/firebase/firestore-translate-text): 國際化處理套件
* [print-folder-tree](https://blog.dmoon.tw/print-folder-tree-on-mac/): 印出資料樹狀結構
* [pretty TS errors](https://github.com/yoavbls/pretty-ts-errors): TS人看的報錯
* [Day.js](https://day.js.org/): 處理時間的套件
* (setting)自動刪除沒用到的import,並排序
```
"editor.codeActionsOnSave":{
"source.organizeImports":true
}
```
* [joyride](https://www.npmjs.com/package/react-joyride): 新用戶guide引導
* [scrolltrigger GSAP](https://greensock.com/react-advanced): 首頁用滾動觸發
* [toastify](https://www.npmjs.com/package/react-toastify): 提示小彈窗套件
* [Animate.css](https://animate.style/): className直接套用css動畫
* [anime.js](https://animejs.com/): 駭客感動畫
* [SweetAlert](https://sweetalert.js.org/) | [SweetAlert 2](https://sweetalert2.github.io/): 精美客製化彈窗 | [也可以做toast](https://sweetalert2.github.io/#didDestroy) | [用react內容](https://sweetalert2.github.io/recipe-gallery/sweetalert2-react.html)
* [codeGPT](https://github.com/appleboy/CodeGPT) GPT幫你寫commit
* [postcss](https://postcss.org/) 讓tailwind不會報錯
* [classnames](https://www.npmjs.com/package/classnames): className管理套件
* [tailwindcss-classnames](https://www.npmjs.com/package/tailwindcss-classnames): tailwind className管理套件
* [tailwind](https://tailwindcss.com/docs/flex-basis) 好用的Atomic CSS,[react的darkmode設定](https://jeffjadulco.com/blog/dark-mode-react-tailwind#june-2021-update)
* [vscode styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) : styled component簡寫
* [bracket select](https://marketplace.visualstudio.com/items?itemName=chunsen.bracket-select) 選取括號裡全部的內容
* [better comments](https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments) 有顏色的註解
* [codeTime](https://www.software.com/product/code-time) 紀錄寫code時間
* [codeSnap](https://marketplace.visualstudio.com/items?itemName=adpyke.codesnap) 快速生成筆記用的圖
* [Import Cost](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost) 顯示import的套件大小,太大會變紅色
* [Image Preview](https://marketplace.visualstudio.com/items?itemName=kisstkondoros.vscode-gutter-preview) hover到img url會顯示圖片
* [es7+ react/redux/react-native snippets](https://ithelp.ithome.com.tw/m/articles/10266011) react常用代碼簡寫
* [IndentRainbow](https://marketplace.visualstudio.com/items?itemName=chingucoding.IndentRainbow) 縮排上色
* [Prettier](https://prettier.io/) | [Prettier config安裝問題](https://stackoverflow.com/questions/70387394/prettier-invalid-configuration-file-even-though-the-file-is-straight-from-the-d) 整理code style,可設定每次存檔自動執行
* [Beautify](https://marketplace.visualstudio.com/items?itemName=HookyQR.beautify)同上,更適用HTML,CSS
* [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) 查這行code是誰寫的
* [Auto Rename Tag](https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag) Tag前後會一起改
* [emmet](https://docs.emmet.io/cheat-sheet/) cheat-sheet (VScode內建)
* [Rest Client](https://www.gss.com.tw/blog/%E4%BD%BF%E7%94%A8-rest-client-%E4%BE%86%E6%B8%AC%E8%A9%A6-api-vs-code%E6%93%B4%E5%85%85%E5%A5%97%E4%BB%B6) 測試api
#### VScode指令
* `/* eslint-disable */`可以關掉eslint報錯
* `/* global FB */`在目前檔案宣告`FB`這個變數
* command+點擊變數可以跳轉到變數被定義/使用的位置
* `fn + F2` | `點擊變數 > 右鍵 > Change All Occurrences`:可以一次修改整個scope的變數名
* `shift + command + p`找指令和設定
* `ctrl+/`: 變註解
* `command+{ }`: 整段縮進排
#### Chrome套件
* [wappalyzer](https://chrome.google.com/webstore/detail/wappalyzer-technology-pro/gppongmhjkpfnbhagpmjfkannfbllamg?hl=zh-TW):可以查看當前網頁的技術
#### DevTools
* Lighthouse:測網頁的表現分數

* 用DevTools檢測network看是否會一直重複打API
* 可以調整**網速**和**斷網**
* API掛掉 === 先連到伺服器再斷網
* 網頁長截圖 `CTRL + Shift + P > Capture full size screenshot`
#### Git 指令
* 如果push到remote之後有改branch名稱,需要unset一次重新抓
```
git branch --unset-upstream
```
* [git clean](https://www.educative.io/answers/how-to-remove-untracked-files-in-git) : 刪除untrack files
* [git reset](https://gitbook.tw/chapters/using-git/reset-commit)
```
git reset --soft HEAD~2 //退兩個commit
```
* git rebase
```
git log --oneline //列出commits
git rebase -i <sha碼> //從哪之前開始修改
// 如果是自己的origin可以git push -f
```
* `git config --local core.ignorecase false`: git預設會忽略大小寫,用此指令更改設定檔 | [清除修改名稱的檔案](https://hackmd.io/@spyua/S1YuFYBJs)
* `git rm -r --cached <file>`可移除git追蹤的檔案,搭配.gitignore使用可以刪掉遠端分支的檔案
* `git add .`:加入當前目錄全部檔案 | `git add --all`:加入根目錄裡全部檔案
* `github clone`會自己抓origin
* `git checkout -b <new branch name>` 創建並直接切到分支
* `git pull` 預設strategy是`merge`
#### termimal
* `ctrl+u`: 刪整行
* `上`: 重複上一則訊息
#### vim
```
1.開啟 終端機
2.指令 vim VimSE.txt (檔案名稱)
3.i 插入模式
4.輸入一些文字 : Hello World
5.ESC 退出
6.:w 儲存
7.:q 結束
```
#### ChatGPT
[7個實用套件](https://www.youtube.com/watch?v=9Pv_9yZ4VaI)
## 情境
* [jest](https://jestjs.io/):測試框架,一般開發,會邊開著測試隨時監聽檔案
* subview Height:排除瀏覽器預設header/footer的高度,[一些css屬性](https://dev.to/frehner/css-vh-dvh-lvh-svh-and-vw-units-27k4),[問題解釋](https://youtu.be/veEqYQlfNx8?t=202)
* [Unix Time Stamp](https://www.unixtimestamp.com/): 一般會用這種資料格式儲存時間戳記,方便跨時區轉換 | [教學](https://blog.techbridge.cc/2020/12/26/javascript-date-time-and-timezone/)
* [.env.local設定全域變數](https://medium.com/itsoktomakemistakes/create-react-app-custom-global-variables-452caa132b95),把api key放這邊不要push到github
* cra source map 可以追蹤網頁原始碼,可以關掉
* [React Error boundary](https://zh-hant.reactjs.org/docs/error-boundaries.html) 抓到錯誤,避免白頁
* [Monolith vs Multi-Repo vs Monorepo](https://www.cythilya.tw/2023/01/28/monolith-vs-multi-repo-vs-mono-repo/) 前後端git管理方法
* 不同的protocal: Http / websocket(可以雙向連線)
* Run Time:使用者在使用時生成 如styled component
Build Time:`npm run build`時生成 如[Tailwind](https://tailwindcss.com/) css
* codeSandBox業界比較常用,可以寫快速建立React / Node.js開發環境
* `npm install --save-dev` : 自動加入有用到的模組到package.json 和 package-lock.json
* comments可以依需求加上`//TODO:`或`//FIXME:`
* 寫code之前要有預期跟驗證
* (問題切小)可以用click事件先取代其他如scroll事件,先單純解決function問題再來處理event
* 可以在global宣告變數,用來儲存function A的資料給function B使用
* API的next_paging可以用來判斷是否要執行fetch功能
* 開發前要看自己在哪個branch
* 中文字體可以用NotoSanse TC
* css可以先設定color system
```
:root{
--white: #fff;
}
div{
color: var(--white);
}
// 設定客制顏色系統
```
* 發pr可寫支援多少web size
* top down vs bottom up
* 查詢目前使用中的 port 及 process id
`lsof -n -i | grep LISTEN`
查詢特定 port 的服務,以 3000 port 為例
`lsof -n -i:3000 | grep LISTEN`
* 處理圖片加載方法:[PreLoad 預加載](https://shubo.io/preload-prefetch-preconnect/) | [LazyLoad延遲加載]() | [skeleton]() | [compress image]() | 檔案格式[AVIF]() [SVG]()
* PORTACAL路徑比較
`../?category=women`:used to reference the parent directory of the current,can be repeated multiple times `../../`
`./?category=women`: starts from the current directory
`/?category=women`: starts from the root directory
`?category=women`: a relative path that does not specify the current or root directory. It is assumed to be relative to the current page's URL
* branch conflict發生原因(兩個commit同時修改到同一行code),解衝突就是**選擇或修改成最終版本**
在VScode解衝突UI比較好
* bug會分等級,通常前面的feature有bug,會再開一個feature/fixbug
* 搜尋可以不依賴api,可以在前端做filter,例如[DevDocs API](https://devdocs.io/)
* [elastic search](https://www.elastic.co/?ultron=B-Stack-Trials-APJ-Exact&gambit=Stack-Core&blade=adwords-s&hulk=paid&Device=c&thor=elastic%20search&gclid=CjwKCAiA_6yfBhBNEiwAkmXy5yL5eZvZrFViIIPFi_W5vDlLESlEHP6pBTLLCHQ9qjgaS6X_j_CAjxoCwbkQAvD_BwE) | [algoliasearch](https://www.npmjs.com/package/algoliasearch)專做搜尋服務
* 如何正確清除eventListener
```
let scrollHandler = () => loadProducts(); //不能用匿名func,不然removeEventListener會抓不到
window.addEventListener("scroll", scrollHandler);
if(條件) {
window.removeEventListener("scroll", scrollHandler);
}
```
* promise 和 async/await 是兩套不同的 coding style,建議統一寫法,全都用 .then/.catch 或是全都用 await/try-catch 來處理
* Semantic HTML: google爬蟲 政府專案 a11y
* 行銷用[SEO](https://yoast.com/image-seo-alt-tag-and-title-tag-optimization/) 寫一個假的h1 logo讓他`display: none;`,增加被google抓到的機率,`img`或 `alt`爬蟲才爬的到
* 背景圖片可用`img`的`object-fit`屬性
* a11y: 聽障視障友好(a跟y中間有11個字的縮寫)
* 小技巧:可以用border來加外框看元素位置,可以隱藏沒在工作中的元素
# Appworks專案筆記
## co-work
firebase重新整理會導去404 page解法:
到firebase.json裡的hosting object新增以下代碼
```
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
```
[抓取URL資料](https://molly1024.medium.com/react-%E5%A6%82%E4%BD%95%E6%8A%93%E5%8F%96url%E7%B6%B2%E5%9D%80%E4%B8%AD%E7%9A%84%E5%8F%83%E6%95%B8-how-to-get-params-in-url-f8902a4256ca)
## Dive into topics
要做[Wordle](https://www.nytimes.com/games/wordle/index.html)
看 [thinking in react](https://beta.reactjs.org/learn/thinking-in-react#step-1-break-the-ui-into-a-component-hierarchy-step-1-break-the-ui-into-a-component-hierarchy)
* TypeScript
* useReducer
* tailWind
phase 0-1
最多六次
用鍵盤打字就好
a~z/可一個一個清除/enter送出
鍵盤optional
1.先切版 prop先設計好
都還沒輸入
輸入未送出
輸入有對:綠色
輸入不對:灰色
輸入有但位置不對:黃色
2.設計資料結構
讓資料結構轉換成看報的畫面
## Topic Discuession
每題報告+討論時間0.5hr
準備簡報 或 範例程式碼
選出人氣組別
#### Phase 1
每組分到兩題,週五下午14:00-18:00報告
* Programiming
1. OOP: prototype / 可以用ES6的class當範例 (搭配sample code) <-
2. functional programming (搭配sample code)
3. 解釋`0.1 + 0.2 !== 0.3`
4. Big O notation 程式碼效能
* Browser
1. How browseer rendering work? (DOM, CSSOM, Render Tree)
2. "event loop" in browser 非同步 瀏覽器記憶體
3. "event propagation" in browser DOM 事件處理,解釋原生JS並帶到React(搭配sample code) <-
4. CORS in browser 遇到error,瀏覽器怎麼去處理?遇到時怎麼解
#### Phase 2
[測驗結果](https://docs.google.com/forms/d/e/1FAIpQLSe918smeHJeeixM8Ofu9n9aKGEbDzekKOqR7X61j3NFCgFFcA/viewscore?viewscore=AE0zAgB1_px3z8ii0_opi-0Buknxp6mglIMXTt6vsd7qlHILLvhhQerDyu2X8fM6u7xSlxE)
每組分到兩題,週一下午10:00-18:00報告,最後有測驗題(半小時寫完)
Network & React (30分鐘)
1. 介紹
2. 格式
3. 即時溝通的方法 比較 優缺點
4. 介紹 <-
Libraries 下個活動有機會用到 (40分鐘)
1. 小工作營 <-
2. 要先介紹
3. styled component 小工作營
4. 管理state/ redux 小工作營
## FireStore
#### 重要基礎概念
* 監聽 OnSnapshot
* 新增 setDoc / addDoc
* 取得 getDoc
* 搜尋 collection / doc
* 複合搜尋 query / orderBy / where / pagenation(分頁)
* 同collection跨doc去抓sub-collection: collection group
* 一開始設計資料結構很重要,把需求情境想好
* Data migration 修改資料結構
* 處理升版可以import from, firebase/[compact](https://firebase.google.com/docs/web/modular-upgrade)/...
### Phase 2
先在資料庫件collection
用id/email/名稱搜尋好友
發送邀請
接受或拒絕
成為好友(單方面?雙向?)
搜尋文章:用tag或好友搜尋文章
討論資料結構(subcollection?)
驗收:發好友邀請 確認 搜尋
### Phase 1
#### Firebase Build-主要產品
* 目前用firebase 9,[Doc](https://firebase.google.com/docs/reference)是用TypeScript寫的,可用[Transform.tools](https://transform.tools/typescript-to-javascript)翻譯
* [Firebase Cloud Firestore](https://firebase.google.com/products/firestore):[這次作業練習] Store and sync app data at global scale
* [Firebase Authentication](https://firebase.google.com/products/auth):會員系統
* [Cloud Storage for Firebase](https://firebase.google.com/products/storage):雲端空間
要小心useEffect不要把伺服器打爆
資料庫分類:SQL跟noSQL
使用firebase的SDK來打api
每一組都要連到同一個資料庫(要開權限)
每個人各自做一個介面,都連到同個資料庫,資料庫結構要共同討論出來
注意大小寫要follow spec
驗收:A發文,ABC要即時顯示文章
3個input component:title/content/tag(固定三種,用radio/下拉式來做)
* id是辨識用的,沒有閱讀意義,文章id可以借用firebase生成的id來用,component也可以用這個id當key

* Local時間:`Timestamp.now()`/`new Date()`/
Server時間:[ServerTimestamp](https://firebase.google.com/docs/reference/kotlin/com/google/firebase/firestore/ServerTimestamp)概念:不要瀏覽器生時間,可能會不準,所以要用server提供的方法
* Tag:可以用數字存,
* Vanilla瀏覽器不能直接import node.module的東西,React可以是因為webpack在做這件事
## Project 1 rewiew
1. 發sample code
2. 14題根據sample的題目,週一報告
3. 一題報告+討論共10分鐘
## 週五進度
* 串GA
* [SEO](https://developers.google.com/search/docs/fundamentals/seo-starter-guide?hl=zh-tw) 100分: 手機/桌機 首頁 商品頁 結帳頁
技術:重要的內容放在重要的位置,例如title,h1
好的內容
* 閱讀文章
[thinking in react](https://beta.reactjs.org/learn/thinking-in-react):先亂做完phase 1再來看這個會很有感
[keeping component pure](https://beta.reactjs.org/learn/keeping-components-pure)
## 週三四進度 接fb/tabpay
[Facebook 登入教學](https://www.tsg.com.tw/blog-search-detail2-239-0-fb-abnormal.htm)
[參考教學](https://penueling.com/%e7%b7%9a%e4%b8%8a%e5%ad%b8%e7%bf%92/vue3-%e4%bd%bf%e7%94%a8-facebook-login-api/)
第一次進來:login
* 判斷local storage有沒有token 2
* 沒有得話顯示沒設計圖的頁面
* 還沒登入時顯示一個facbook按鈕
* 透過sdk跟fb溝通
* 拿到token 1
* 用token 1打 Backend API 換token 2
* token 2存到local storage(下次進來會知道有沒有登入過)
* 登入完成會顯示大頭照,名字,email
第二次進來:
* 用local storage裡的token 2 拿profile資料
上線:
* [申請隱私權政策網址](https://www.privacypolicies.com/)
## 週二進度
購物車下半部-訂購資料
* 熟悉react表單的處理方式(畫面上的東西都由state來控制)
關鍵字:controlled component/uncontrolled component
* 判斷輸入正確性
* input要綁定state
* (Optional)form format validation驗證輸入內容/按下確認付款檢查有沒有漏填/提示
## 週一進度
購物車上半部
用購物車的id打api,一個產品打一次
瀏覽器可以同時發出的request數量:跟http版本有關
1. 修改某個商品數量:下拉選單(受庫存影響)
2. 刪除欄
3. 點圖片回到商品頁
4. 同品項要合併(加入購物車時先把local storage拉出來比較,再塞進去)
5. 小icon資料都要更新
6. 右下角金額計算,購物車若是空的,運費也要為0
## 今日進度
手機桌機一起切版
使用[react router DOM](https://www.npmjs.com/package/react-router-dom)來做購物車分頁
router的原理是history api
可以用form切表單
找同學review
## 商品頁&加入購物車
#### 基本規格
* 沒選顏色或尺寸或數量,增加鍵會block
舉例:尺寸的狀態能不能被按 要判斷1.有沒有選顏色2.有沒有庫存
* 顏色>選尺寸>選數量
* 先選顏色才能選尺寸,選了顏色會對應每個尺寸有沒有庫存
* 增加購物車會存到local storage
* 庫存數量要扣掉增加到購物車的數量,超過counter沒辦法增加
* 首頁/商品頁購物車小icon要顯示總件數
#### [Local Storage](https://5xruby.tw/posts/localstorage)介紹
* 只能儲存字串,所以可以用JSON的方法來設計要存入的資料,再在其他地方parse回來處理
```
JSON.stringfy();
JSON.parse();
```
#### stylish產品頁架構/處理
* 根據網址上的id fetch對應商品資料/用local storage更新資料
```
import React, { useState, useEffect } from "react";
const searchParams = new URLSearchParams(window.location.search);
const id = searchParams.get("id");
function ProductContent(props) {
const [details, setDetails] = useState(undefined); //商品資料,state為JSON object
const [selectColor, setSelectColor] = useState(null); //選擇的顏色,state為色碼字串,"#CCCCCC"
const [colorStock, setColorStock] = useState({}); //選擇顏色各尺寸的庫存,state為{S:6 M:0 L:4}
const [selectSize, setSelectSize] = useState(null); //選擇的尺寸,state為尺寸字串,"S"
const [stockLeft, setStockLeft] = useState(0); //選擇品項(顏色尺寸)的庫存數量,state為number數量
const [amount, setAmount] = useState(0); //需求數量,state為number數量
const [shouldUpdateStockViaLocal, setShouldUpdateStockViaLocal] = useState(false); //設置一個flag,目的是只有在refresh後才需用local storage改變庫存,但要避免無限loop
useEffect(() => { //載入商品頁會觸發fetch事件
async function loadData() { //定義功能
try {
const res = await fetch(
`https://api.appworks-school.tw/api/1.0/products/details?id=${id}`
);
const json = await res.json();
setDetails(json.data); //把資料存入state
setShouldUpdateStockViaLocal(true); //載入資料後flag設為true
} catch (error) {
console.log(error);
}
}
loadData(); //呼叫功能
}, []);
useEffect(() => {
if (shouldUpdateStockViaLocal) {
updateStockViaLocal(); //更新庫存功能
setShouldUpdateStockViaLocal(false); //flag設為false,可擋掉之後改變資料的事件
}
}, [details]); //綁定在資料發生改變時觸發
if (details === undefined) { //因為fetch是非同步的,如果還沒fetch到商品資料
return <div>Loading...</div>;
}
...//其他function & function component...
return (
<ProductSpec>
...
<Colors /> //顏色選擇器
<Sizes /> //尺寸選擇器
<Counter /> //計數器
<AddCartBtn //加入購物車鈕
stockLeft={stockLeft}
onClick={() => {
addToCart();
handleCartAmount(calcCart);
}}>加入購物車
</AddCartBtn>
...
</ProductSpec>
);
}
export default ProductContent;
```
#### 顏色選擇器
* **注意**:用同一個`.map()`生成的component需要帶入一個key={id}屬性,id須為獨立的(資料型別reference的原因)
* 因為顏色是第一個選的,為避免尺寸選到一半切換顏色,因此點擊顏色時要清空之前選擇的資料,以免加入購物車鈕可以按
* 產生“選擇的顏色各尺寸庫存”資料,範例:`{S:6 M:0 L:4}`給尺寸選擇器判斷用
```
function Colors() {
return details.colors.map((color, index) => {
return (
<ColorSelector
key={index}
selected={selectColor === color.code} //是否被選到,傳props給styled component判斷用
onClick={() => {
setSelectColor(color.code); //設定選擇的顏色
setColorStock(getStockByColor(color.code)); //存入選擇顏色後各尺寸的庫存資料,給尺寸選擇器判斷用,用callback生成資料
setAmount(0); //清空計數器數量
setSelectSize(null); //清空尺寸
setStockLeft(0); //清空庫存資料
}}
>
<Color
key={index}
title={color.name}
colorCode={`#${color.code}`}
></Color>
</ColorSelector>
);
});
}
function getStockByColor(colorCode) { //生成選擇的顏色各尺寸庫存數量資料
const selectedColor = details.variants.filter(
(item) => item.color_code === colorCode
);
return selectedColor.reduce((obj, item) => {
obj[item.size] = item.stock;
return obj;
}, {});
}
```
#### 尺寸選擇器
* 根據“選擇的顏色各尺寸庫存”資料,範例:`{S:6 M:0 L:4}`判斷是否能選擇當前尺寸,選擇後把“此品項庫存數”存到state給計數器判斷
* 為避免中途切換尺寸,計數器當前數量超過“此品項庫存數”,故在點擊尺寸時也先將計數器歸零
```
function Sizes() {
return details.sizes.map((size, index) => {
return (
<Size
key={index}
selected={selectSize === size} //給styled component判斷用
stock={colorStock[size]} //給styled component判斷用
onClick={() => {
if (colorStock[size] > 0) { //如果“此品項庫存資料”為0則無法點擊
setSelectSize(size); //設定選擇的尺寸
setStockLeft(colorStock[size]); //存入“此品項庫存數”給計數器判斷
setAmount(0); //計數器歸零
}
}}
>
{size}
</Size>
);
});
}
```
#### 計數器
* 根據“此品項庫存數”判斷最多能加入的“購買數量”
```
function Counter() {
return (
<CounterBlock>
<Operator onClick={() => setAmount((prevAmount) => prevAmount > 0 ? (prevAmount -= 1) : prevAmount)}>
-
</Operator>
<Amount>{amount}</Amount> //購買數量展示
<Operator onClick={() => setAmount((prevAmount) => prevAmount < stockLeft ? (prevAmount += 1) : prevAmount)}>
+
</Operator>
</CounterBlock>
);
}
```
#### 加入購物車鈕
* 將“選擇品項資料”和“購買數量”和“剩餘庫存”資料存入local storage
* local storage設計必須能合併同品項,直接加上數量,而非再新增一筆
```
function addToCart() {
if (stockLeft > 0 && amount > 0) { //“此品項庫存數”跟數量都>0才能按
//local storage的資料結構設計
const item = {
productId: details.id,
qty: amount,
colorCode: selectColor,
size: selectSize,
newStock: stockLeft - amount,
};
const cart = JSON.parse(localStorage.getItem('cart')) || [];
const index = cart.findIndex(
(cartItem) =>
cartItem.productId === item.productId &&
cartItem.colorCode === item.colorCode &&
cartItem.size === item.size
);
if (index === -1) {
cart.push(item);
} else {
cart[index].qty += item.qty;
cart[index].newStock = item.newStock;
}
localStorage.setItem('cart', JSON.stringify(cart));
//callback更新“選擇品項”的庫存數量
updateStock(selectColor, selectSize, stockLeft - amount);
//清空所有選擇資料
setAmount(0);
setSelectSize(null);
setSelectColor(null);
setColorStock({});
}
}
//更新庫存功能,參數為“選擇的顏色”,“選擇的尺寸”和“更新的庫存數量”
const updateStock = (colorCode, size, newStock) => {
setDetails((prevDetails) => {
const newVariants = prevDetails.variants.map((variant) => {
if (variant.color_code === colorCode && variant.size === size) {
return { ...variant, stock: newStock };
}
return variant;
});
return { ...prevDetails, variants: newVariants };
});
};
...
return (
<ProductSpec>
...
<AddCartBtn //加入購物車鈕
amount={amount} //給styled component判斷用
onClick={() => {
addToCart(); //加入購物車callback
handleCartAmount(calcCart); //計算購物車總數並傳給父層,讓購物車logo顯示商品總數
}}>加入購物車
</AddCartBtn>
...
</ProductSpec>
);
```
#### 購物車logo顯示商品總數
* 要安裝[PropTypes](https://blog.logrocket.com/validate-react-props-proptypes/),並在component檔裡設定來validat props
* 商品頁處理:
```
//ProductContent.js(子層A,處理資料並用callback傳給父層)
import PropTypes from "prop-types"; //安裝propTypes
function ProductContent(props) { //父層傳入的props
...
//把資料傳給父層用的function
const handleCartAmount = (data) => {
props.getCartAmount(data); //使用從父層傳來的props function
};
//抓取local storage裡的商品總數量
function calcCart() {
const cart = JSON.parse(localStorage.getItem('cart')) || [];
const sum = cart.reduce((accumulator, item) => {
return accumulator + item.qty;
}, 0);
return sum;
}
...
}
//用propTypes認證props
ProductContent.propTypes = {
getCartAmount: PropTypes.func.isRequired,
};
```
```
//App.js(父層,控制state)
import React, { useState, useEffect } from "react";
function App() {
redirectToHomePage();
const [cartAmount, setCartAmount] = useState(null);
useEffect(() => { //載入時會根據local storage更新購物車數量,refresh才能保留狀態
const cart = JSON.parse(localStorage.getItem('cart')) || [];
const sum = cart.reduce((accumulator, item) => {
return accumulator + item.qty;
}, 0);
setCartAmount(sum);
}, []);
const getCartAmount = setCartAmount; //根據子層處理的資料修改state的callback
return (
<Container>
<Header cartAmount={cartAmount} /> //數量傳給子層B
<ProductContent getCartAmount={getCartAmount} /> //把callback當成props傳給子層A
<Footer cartAmount={cartAmount} /> //數量傳給子層B
</Container>
);
}
```
```
//Header.js(子層B,接收資料) //Footer.js同
import PropTypes from "prop-types";
function Header({ cartAmount }) { //解構賦值父層傳入的props
...
return(
...
<div className="cart-count">{cartAmount}</div> //顯示數字
...
);
}
Header.propTypes = {
cartAmount: PropTypes.number.isRequired,
};
```
* 首頁處理:
```
//index.js(首頁的購物車處理)
const cartCount = document.querySelectorAll(".cart-count");
function showCartAmount() {
const cart = JSON.parse(localStorage.getItem('cart')) || [];
const sum = cart.reduce((accumulator, item) => {
return accumulator + item.qty;
}, 0);
cartCount.forEach((cartLogo) => (cartLogo.textContent = sum));
}
showCartAmount();
```
#### 用Local Storage裡的資料更新庫存
* refresh頁面觸發的callback,見架構
```
//從local storage取得顏色,尺寸,剩餘庫存資料,再利用updateStock()更新庫存
const updateStockViaLocal = () => {
if (details === undefined) {
return;
}
const cart = JSON.parse(localStorage.getItem('cart')) || [];
for (let i = 0; i < cart.length; i++) {
const { colorCode, size, newStock } = cart[i]; //用解構賦值取出參數,再更新庫存數量
updateStock(colorCode, size, newStock);
}
};
```
#### styled component根據props判斷顯示顏色
```
export const Size = styled.div`
...
color: ${({ stock, selected }) => {
if (selected && stock) {
return "white";
} else if (stock === 0) {
return "rgba(63, 58, 58, 0.25)";
}
return "black";
}};
background-color: ${({ stock, selected }) => {
if (selected && stock) {
return "black";
} else if (stock === 0) {
return "rgba(236, 236, 236, 0.25)";
}
return "#ECECEC";
}};
...
&:hover {
color: ${({ stock }) => (stock === 0 ? "rgba(63, 58, 58, 0.25)" : "white")};
background-color: ${({ stock }) => stock === 0 ? "rgba(236, 236, 236, 0.25)" : "black"};
}
`;
```
## 首頁連結到各產品頁
### 基本規格
* 首頁點擊商品圖片或輪播圖會導到對應的商品頁
* 商品頁搜尋功能正常(包含RWD)
#### 商品頁搜尋功能 & RWD
```
//Header.js
function Header() {
const [keyword, setKeyWord] = useState("");
const [onSearch, setOnSearch] = useState(false); //控制搜尋欄開啟/關閉的state
const [display, setDisplay] = useState({}); //搜尋顯示效果的state
useEffect(() => { //根據搜尋欄state決定要用哪一組className物件
if (onSearch) {
setDisplay({
logo: "logo hide",
img: "show",
search: "search show",
input: "search-input show",
submitBtn: "submit-btn show",
searchIcon: "search-icon hide",
searchImg: "search-img hide",
});
} else {
setDisplay({
logo: "logo",
img: "",
search: "search",
input: "search-input",
submitBtn: "submit-btn",
searchIcon: "search-icon",
searchImg: "search-img",
});
}
}, [onSearch]); //搜尋欄state變化時觸發useEffect
//搜尋
const inputValue = (e) => { //處理輸入事件的字串
setKeyWord(e.target.value.replace(/\s/g, "")); //排除空值後更新state
};
const search = (e) => {
e.preventDefault();
if (keyword === "") { //輸入為空值時不能按
return;
}
window.location.href = `products.html?keyword=${keyword}`; //引導到首頁,並加入keyword
};
const pressEnter = (e) => {
if (e.key === "Enter") { //如果按鍵是Enter也會觸發搜尋事件
search(e);
}
};
return (
<header>
...
<div className="close-search" onClick={() => setOnSearch((prevState) => !prevState)}> //用state來控制className以使用原先的css檔
<img className={display.img} src="images/cross.png" />
</div>
<a className={display.logo} href="products.html?category=all">...</a>
...
<div className={display.search}>
<input
type="text"
placeholder="search..."
className={display.input}
onKeyUp={pressEnter} //有按按鍵時觸發pressEnter()
onInput={inputValue} //輸入時觸發input()
/>
<div className={display.submitBtn} onClick={search}>...</div> //點擊觸發search()
<div className={display.searchIcon} onClick={() => setOnSearch((prevState) => !prevState)}> //(手機端)點擊 開啟/關閉 搜尋欄位
<img className={display.searchImg} src="images/search.png" />
</div>
</div>
...
</header>
);
}
```
#### 商品圖 & banner做redirect
* 商品圖
```
//index.js
container.insertAdjacentHTML(
"beforeend",
`
...
<div><a href="/?id=${product.id}"><img src="${product.main_image}"></a></div> //商品圖<img>外包一層<a>,導到根目錄+id
...
`
);
```
* 輪播圖
```
//banner.js
const bannerBtn = document.querySelector(".banner-btn");
function initSlider() {
fetch("https://api.appworks-school.tw/api/1.0/marketing/campaigns")
.then((response) => response.json())
.then((json) => {
let dotHTML = "";
json.data.forEach((item) => {
...
const poemHtml = `
<div class="poem" data-image=${item.picture} data-id=${item.product_id}> //把產品id放入data-*帶入
...
</div>
`;
...
});
...
}
function showSlide(prevClickSlide) {
...
poem.forEach((poemtext, index) => {
if (index === currentSlide) {
...
setTimeout(() => {
...
}, 500);
bannerBtn.addEventListener("click", () => onBannerClick(poemtext)); //綁定事件
} else {
...
}
});
...
}
function onBannerClick(data) { //事件call-back,redirect到產品頁
window.location.href = `/details?id=${data.dataset.id}`;
}
```
#### 初次載入產品頁,無產品id,則導回首頁(refresh)
* 因預設的網址是產品頁,如果沒有產品id會載不出來,所以寫一個功能載render時判斷網址有沒有產品id,沒有就導到首頁
```
//App.js
const productId = document.location.search;
function redirectToHomePage() {
if (!productId) {
window.location.href = '/products.html';
}
}
function App() {
redirectToHomePage();
return (
...
);
}
```
## React 切版 & 拆組件
#### stylish切版處理
* [Fragment](https://zh-hant.reactjs.org/docs/fragments.html) : React預設return用的空parent element
* component 檔名第一個字母也會大寫(和 component 同名),從檔名可看出是 component 還是一般的 js 檔
* 一個 component 一個檔案
* styled component 命名避免縮寫
* styled component 技巧
```
//styled component 保留可辨認的className
import styled from "styled-components/macro";
//用hr做分隔線
const SplitLine = styled.hr`
border:none;
border-top:1px solid #3f3a3a;
flex-grow: 1;
`;
//傳props判斷顏色
const Size = styled.div`
border: ${(props) => (props.selected ? "1px solid #979797" : "none")}; //三元判斷
color: ${(props) => {
if(props.selected) { //if...else判斷
return "white";
} else if(props.lowStock) {
return "rgba(63, 58, 58, 0.25)";
}
return "black";
}};
`;
function ProductContent() {
return (
<Size selected>S</Size>
<Size>M</Size>
<Size lowStock>L</Size>
);
}
//繼承
const Color1 = styled.div`
width: 24px;
height: 24px;
background-color: white;
`;
const Color2 = styled(Color1)`
background-color: pink;
`;
const Color3 = styled(Color1)`
background-color: purple;
`;
// & 增加屬性
const Color = styled.div`
background-color: white;
&:hover{
background-color: blue;
}
`;
```
#### 拆component,匯入匯出
* export Default
匯入

匯出

* export
匯入

匯出

## 使用React
#### React環境設定
1. 先把當前可能會跟CRA衝突的資料夾和檔案改檔名:stylish -> project / index.html -> products.html
2. 在根目錄執行`npx create-react-app stylish`
3. 把原本專案/public裡的檔案移到CRA的stylish/public
4. 把原專案根目錄的設定檔移入stylish(例如firebase/eslint/prettier等)
5. 進入stylish資料夾,npm start測試看看,在網址後面加上/products.html會載入原網頁
6. `npm init @eslint/config`新增eslint設定檔,並在rules裡加入老師的規則

#### firebase部署設定
1. firebase.json裡修改部署檔案路徑為build資料夾
```
{
"hosting": {
"public": "build",
}
}
```
2. `npm run build`轉譯public文件,並生成build資料夾,裡面放轉譯後的HTML CSS JS檔(瀏覽器只會讀這個)
3. `firebase deploy`部署到firebase伺服器(firebase設定檔要放對地方,設定的快捷指令也記得加到package.json裡)
4. 測試: 輸入網址firebase伺服器網址會出現react的畫面,URL後加上/products.html會載入原網頁
#### 產品頁面(修改新的index.html & App.js & App.css)
* index.html安裝NotoSans TC字體,並刪除非必要的程式碼,產品頁會呈現在這裡
* App.css先複製要沿用的header/footer/mobile-footer
* 在App.js裡製作產品頁,`App`元件會被輸入到index.html的`root`,所以要注意相對檔案路徑(如image的路徑
* `App()`生成的`<產品分類的tag>`裡,路徑要修改:
```
href="?category=women" -> href="products.html?category=women"
```
## 重構
Knowing where to refactor within in a system is quite a challenge to identify areas of bad design. These areas of bad design are known as "Bad Smells" or "Stinks" within code. For examples:
* Duplicate code
* Dead Code: 沒人用的code,沒必要的檔案
* Long Method: 一個function只做一件事
* Comments: self documented code
* Inappropriate Naming: 避免縮寫
* Magic Number: 數字,但不知道代表的意思 若是常數,全大寫
#### 設定Eslint Prettier
* 新增`.eslintrc.json`config檔 > `npm install eslint --save-dev`
*  可以開啟VScode的setting.json直接修改
* Prettier存檔自動執行,用單引號
```
//直接加在setting.json
"prettier.singleQuote": true,
"editor.formatOnSave": true
```
* firebase-tools 跟 eslint 裝到 devDependencies
* npm run <自訂指令>
```
//可自訂快捷指令
"scripts": {
"deploy": "firebase deploy",
"ptr": "prettier --write ."
},
```
#### Stylish 重構處理
* 把程式碼包入function裡,讓檔案裡沒有任何的程式碼是散在function之外,全部都可以從 function 名稱去知道每一段程式碼在做的事情
* 雖然 function 有 hoisting 的特性,但習慣上還是會把宣告擺在前面,不過也是有另外一派喜歡把宣告擺在後面
* `.replace(/\s/g, '')`可以把頭尾的空格去掉,上一行的`.trim()`是多餘的
* 有的話就做a,沒有就做b,可以用三元運算子
```
const url = keyword ? `${baseURL}search?keyword=${keyword}&paging=${nextPaging}` : `${baseURL}${category}?paging=${nextPaging}`;
```
* 通常 is/has 開頭的變數是用來存放 boolean 值,`isErr`叫 `errorMessage` 比較適合
* 常數用大寫底線表示
```
const ERROR_MESSAGE = {
INVALID_CATEGORY: `Sorry, We don't have this category: ${category} <br> Click <a href="?category=all">HERE<a> to go back`,
UNKNOWN_ERROR: 'Oops, Something went wrong...',
NOT_FOUND: 'Sorry, We don't have'
}
```
* (不是我的)`bannerArray = new Array(dotsNum).fill().map((_, index) => index + 1);`
## 輪播圖
### 基本規格
* fetch banner content from API
* auto-switch banner content every 5 sec
* nav to click for each content
* stop while hover
* cross-fade effect
#### stylish輪播圖處理
* fetch API產生輪播圖
nav點擊導航功能
```
//三層重疊的圖層
const banner = document.querySelector('.banner'); //廣告板,用來做淡入淡出
const subBanner = document.querySelector('.sub-banner'); //用來做殘影的廣告板,放最後
const dotContainer = document.querySelector('.dot-container'); //導航點點容器,放最前,負責click跟hover事件
//state
let poem, navItems, prevSlide, timer; //宣告全局的廣告跟導航點
let currentSlide = 0; //表示當前投影片的值
function initSlider() {
// 取得資料
fetch('https://api.appworks-school.tw/api/1.0/marketing/campaigns')
.then(response => response.json())
// 建立廣告和導航點html
.then(json => {
let dotHTML = '';
json.data.forEach(item => {
const poemText = item.story.split(/\r\n/);
const poemHtml = `
<div class="poem" data-image=${item.picture}>
<p>${poemText.slice(0,-1).join('<br>')}</p>
<span>${poemText.at(-1)}</span>
</div>
`;
banner.innerHTML += poemHtml;
dotHTML += `<div class="dot"></div>`;
});
dotContainer.innerHTML +=`<div class="banner-dot">${dotHTML}</div>`;
poem = document.querySelectorAll('.poem'); //廣告
navItems = document.querySelectorAll('.dot'); //整排導航點
showCurrentClick(navItems); //把點點加上點擊功能
//初始化輪播
showSlide(); //展示投影片
startSlider(); //計時
})
.catch(error => banner.innerHTML=`Sorry, Something went wrong! Status code: ${error.status}`);
}
//載入輪播圖
initSlider();
```
* 新增點擊導航功能到點點上
```
function showCurrentClick(items){
items.forEach((item, index) => { //整排註冊點擊事件
item.addEventListener('click', () => {
const prevClickSlide = currentSlide; //把當前投影片記錄下來
currentSlide = index; //切換當前投影片為點擊的index
showSlide(prevClickSlide); //展示點擊投影片,並把紀錄傳入
});
});
}
```
* 輪播展示功能
fade-in fade-out效果
```
//js
function showSlide(prevClickSlide) {
// 利用殘影banner製造cross-fade
prevSlide = (typeof prevClickSlide === "number")? prevClickSlide : currentSlide - 1; //如果有點擊,用點擊當成前一張,如果沒有,用輪播的前一張
const poemArr = Array.from(poem); //把Node.list轉為Array
subBanner.style.backgroundImage = `url(${poemArr.at(prevSlide).dataset.image})`;
// 切換當前輪播圖片和導航點顏色
poem.forEach((poemtext, index) => {
if (index === currentSlide) {
banner.classList.add('fade-out');
poemtext.classList.add('fade-out');
setTimeout(() => {
banner.style.backgroundImage = `url(${poemtext.dataset.image})`;
banner.classList.remove('fade-out');
poemtext.classList.remove('fade-out');
poemtext.style.display = "block";
}, 500);
} else {
setTimeout(() => {
poemtext.style.display = "none";
}, 500);
}
});
navItems.forEach((navItem, index) => {
if (index === currentSlide) {
navItem.classList.add('active');
} else {
navItem.classList.remove('active');
}
});
}
//css
main{
position: relative;
}
.banner{
opacity: 1;
transition: opacity 0.5s ease-in-out; //圖片透明漸變時間
z-index: 0;
}
.fade-out {
opacity: 0;
color: rgba(7, 7, 7, 0)
}
.dot-container{
position:absolute;
top:0;
right:0;
z-index: 1;
}
.sub-banner{
position:absolute;
top:0;
right:0;
z-index: -1;
}
.poem{
display: none;
transition: color 0.5s ease-in-out; //文字透明漸變時間
}
```
**TIPS**:運用HTML5屬性[`data-`](https://pjchender.blogspot.com/2017/01/html-5-data-attribute.html)來儲存資料
```
//html
<div class="poem" data-image=${url資料}>
//js
banner.style.backgroundImage = `url(${poem.dataset.image})`;
```
* 計時器
```
//開啟計時器
function startSlider() {
timer = setInterval(() => {
currentSlide = (currentSlide + 1) % poem.length; //對currentSlide用廣告數量取餘數,值就不會大於廣告數量,可達到無限循環
showSlide();
}, 5000);
}
//關閉計時器
function stopSlider() {
clearInterval(timer);
}
```
* hover時停止輪播圖
```
// 停止輪播圖
banner.addEventListener('mouseenter', () => {
stopSlider();
});
// 開始輪播圖
banner.addEventListener('mouseleave', () => {
startSlider();
});
```
## 搜尋
### 基本規格
* handle user input with blanks within keyword
* also work with Enter keyup
* won't send search request while nothing or blank input
* mobile default: search bar hidden
* clear input bar while close search bar (mobile only)
* keep search result after refresh page
#### stylish搜尋處理
* 按下搜尋功能
常規表達式處理各處空白
沒輸入東西不會送出請求
把關鍵字保存在瀏覽器url欄位
```
//註冊點擊事件
searchButton.addEventListener("click", onSearchButtonClick);
function onSearchButtonClick() {
searchInput.focus(); //點擊搜尋按鈕也不會讓input欄blur
//取得使用者的輸入並排除空格
const userInput = searchInput.value;
const keyword = userInput.replace(/\s/g, '');
if (keyword === "") { //沒有輸入關鍵字或是只有空格會拒絕搜尋
return;
}
//保存url,更新state
history.pushState(null, null, `?keyword=${keyword}`); //把url儲存在瀏覽器網址
document.querySelectorAll(`a[href='?category=${category}']`).forEach(item => item.classList.remove('active')); //清除當前category顏色
nextPaging = 0; //初始化頁數
container.innerHTML = ""; //清除當前商品欄
loadProducts(); //fetch商品
}
```
**TIPS**:[History API](https://developer.mozilla.org/en-US/docs/Web/API/History): Allows manipulation of the browser session history, that is the pages visited in the tab or frame that the current page is loaded in.
* 按[Enter](http://www.foreui.com/articles/Key_Code_Table.htm)也會觸發搜尋按鈕(key code table有專用的值)
```
searchInput.addEventListener("keyup", onSearchInputKeyup);
function onSearchInputKeyup(event) {
if (event.key === "Enter") {
onSearchButtonClick();
}
}
```
* 重新整理頁面,抓取瀏覽器url欄位保存的關鍵字
```
// 若刷新頁面,抓取之前搜尋保留的關鍵字
const searchParams = new URLSearchParams(window.location.search);
const keyword = searchParams.get('keyword');
// 檢查 URL 是否包含關鍵字,若有,則使用搜尋 API 取得搜尋結果,否則使用分類 API 取得商品
const url = keyword ? `${baseURL}search?keyword=${keyword}&paging=${nextPaging}` : `${baseURL}${category}?paging=${nextPaging}`;
```
* 若搜尋不到商品,顯示沒有商品
```
//loadProducts()
if(products.length === 0){
isErr = errMsg.weDontHave;
load.innerHTML = `<p>${isErr} ${keyword}</p>`;
}
```
* 手機端預設隱藏searchBar,關閉時清除搜尋結果
```
mobileSearch.addEventListener("click", showMobileSearch);
closeSearch.addEventListener("click", hideMobileSearch);
//mobile search toggle
function showMobileSearch(){
searchButton.classList.add('show');
document.querySelector(".close-search img").classList.add('show');
document.querySelector(".search-input").classList.add('show');
document.querySelector(".search").classList.add('show');
document.querySelector(".logo").classList.add('hide');
}
function hideMobileSearch(){
searchInput.value = null;
document.querySelector(".submit-btn.show").classList.remove('show');
document.querySelector(".close-search img.show").classList.remove('show');
document.querySelector(".search-input.show").classList.remove('show');
document.querySelector(".search.show").classList.remove('show');
document.querySelector(".logo.hide").classList.remove('hide');
}
```
**TIPS**:做一個透明的search按鈕疊在放大鏡上,手機端預設時隱藏search按鈕,點放大鏡會開啟searchbar
**TIPS**:手機版搜尋裡的input欄隨父層變化
```
.search-input.show{
width: calc(100% - 60px);
}
```
## 商品類別
### 基本規格
* redirect link to different category API
* UI color change while load current category
* handle edge case (wrong query string)
#### stylish商品類別處理
* URL設計
```
const baseURL = "https://api.appworks-school.tw/api/1.0/products/";
//state
let category = null;
//抓取category
const currentUrl = new URL(window.location.href); //抓取當前瀏覽器網址
category = currentUrl.searchParams.get('category');
//...省略,fetch url:
fetch(`${baseURL}${category}?paging=${nextPaging}`)
```
**TIPS**:`const currentUrl = new URL(window.location.href);`會產生一個URL object,有很多好用的屬性

* 讓點選的nav item變色
```
<header>
<nav>
<a href="?category=home" id="home">Home</a>
<a href="?category=about" id="about">About</a>
<a href="?category=contact" id="contact">Contact</a>
</nav>
</header>
<script>
//抓取網址querystring的值
if (category === null){
category = "all"; //沒有category時設成all
} else {
const currentNavItem = document.querySelectorAll(`a[href='?category=${category}']`); //抓取當前類別ui
currentNavItem?.forEach(item => item.classList.add('active')); //有抓到的每個ui增加class
}
</script>
<style>
.active {
color: red;
}
</style>
```
## fetch商品資料
### 基本規格
* Connect RESTful API with Ajax to create product list
* Do not causing unnecessary fetch request
* Scrolling behavior should be fluent
* show"loading..."while loading
* handle error to let user know
* handle error msg while server down(load網頁後斷網)
#### stylish載入商品處理
* infinity scroll
```
const baseURL = "https://api.appworks-school.tw/api/1.0/products/";
const load = document.querySelector("#loading");
const container = document.querySelector("#container");
//state
let loading = false; //是否正在載入
let nextPaging = 0; //TIPS:把fetch的資料傳到global使用,用來給scroll事件判斷是否要fetchAPI
let isErr;
//抓取category的searchquery,若沒有則設為all
let currentUrl = new URL(window.location.href);
let category = currentUrl.searchParams.get('category');
if (category === null){
category = "all";
}
// 判斷捲動是否到底
let scrollHandler = () => {
if (window.scrollY + window.innerHeight >= document.body.offsetHeight - 300 && typeof nextPaging === "number" && errMsg == "") { //若沒資料API的next_paging會回傳undefined,可以拿來當判斷邏輯
loadProducts();
}
}
window.addEventListener("scroll", scrollHandler);
async function loadProducts() {
if (loading) { // 避免重複請求數據,以免造成重複或不必要的數據請求。
return;
}
loading = true;
load.innerHTML ="<p>Loading...</p>";
const url = `${baseURL}${category}?paging=${nextPaging}`;
try{
const data = await fetch(url);
isErr = "";
if(!data.ok) throw new Error(`HTTP status ${data.status}`);
const json = await data.json();
const products = json.data;
nextPaging = json.next_paging;
renderProduct(products);
if (nextPaging === undefined){ //如果沒有下一頁,清除滑動事件
removeScroll();
}
} catch(error) {
if(error.message === "HTTP status 400") {
isErr = errMsg.badReq;
} else {
isErr = errMsg.other;
}
removeScroll();
load.innerHTML = `<p>${isErr}</p>`;
} finally {
loading = false;
if(isErr !== errMsg.other && isErr !== errMsg.badReq && isErr !== errMsg.weDontHave){
load.innerHTML = "";
}
}
}
const errMsg = { //不同狀況的錯誤訊息
badReq: `Sorry, We don't have this category: ${category} <br> Click <a href="?category=all">HERE<a> to go back`,
other:'Oops, Something went wrong...',
weDontHave: `Sorry, We don't have`
}
function renderProduct(products){
products.forEach((product) => { //每個產品製作HTML
let colorsHTML = "";
product.colors.forEach((color) => { //每個產品有多個顏色
if(color.code){
colorsHTML += `<li class="color" style="background-color:#${color.code}" title="${color.name}"><a href="#"></a></li>`;
}
});
container.insertAdjacentHTML(
"beforeend",
`
<div class="column">
<div><img src="${product.main_image}"></div>
<div class="colors">${colorsHTML}</div>
<p class="product-name">${product.title}</p>
<p>TWD.${product.price}</p>
</div>
`
);
});
}
// 加载第一页的商品
loadProducts();
//remove scroll功能
const removeScroll = () => window.removeEventListener("scroll", scrollHandler);
```
**TIPS**:`-300`可以讓觸發條件在使用者快滑到頁面底部時就加載內容。反之,使用者必須在頁面底部滑過多一點距離,才會觸發加載。
**TIPS**: [`.insertAdjacentHTML(position, text)`](https://developer.mozilla.org/zh-TW/docs/Web/API/Element/insertAdjacentHTML): 可以把文字加入指定位置
## 切版
### 基本規格
* Pixel pefect
* RWD
#### stylish切版處理
* body佔滿視窗
header貼頂
footer貼body底
手機footer貼視窗底
```
body{
min-height: 100vh; //body height要佔滿視窗
display: flex;
}
header{
z-index: 2;
position: sticky; //用sticky不會跟網頁內容脫鉤
top: 0px;
}
footer{
margin-top: auto; //搭配flex推margin-top
}
@media screen and (max-width: 1280px){
.mobile-footer{
position: fixed; //用fixed跟網頁內容脫鉤
bottom: 0;
}
}
```
## Deploy到firebase & 發PR方法
[專案工作流程](https://github.com/AppWorks-School-Materials/Front-End-Class-Batch19/tree/main/week-0%20(by%20Feb%2012)/part-1)
[firebase console](https://console.firebase.google.com/u/0/)
[首頁架構圖](https://www.figma.com/file/0dvdwoQF3hERLCXKJlz8H2/%E9%A6%96%E9%A0%81%E6%9E%B6%E6%A7%8B?node-id=0%3A1&t=ShLPbdCKoF5usoVp-0)
* .gitignore
```
**/node_modules
/source_images
**/api.http
**/.DS_Store
.firebase/
build
coverage
```
## Study Tips
If you find some topics particularly challenging you might need to revisit them. You can use a technique called **spaced repetition**. Take this course today, then tomorrow, then after a further 3 days, then after a week, then after two weeks etc. Doing this will help cement syntax and terminology in your long term memory.
A technique you might want to try is to go through the course, just watching the videos first. Then retake the course again with the videos and assessment.
Another technique would be to keep retaking the assessments at using the **spaced repetition** method.