一個解決「今天到底吃什麼」的美食探索平台:比 Google Map 更豐富的商品資訊、更好查的篩選,再用隨機挑選店家/菜色幫你戰勝選擇困難。
60 秒看懂: 這是一個 React + Vite 前端 + Lumen (PHP) JWT API 後端 的全端專案。後端來自課堂專案,本次重構補上了完整前端,並修掉後端的權限繞過與壞掉的路由。前端內建 demo 模式(種子資料),npm install && npm run dev 就能完整操作,不需要 PHP、不需要資料庫。
| 探索/篩選 | 店家頁 |
|---|---|
![]() |
![]() |
| 隨機挑選 | 登入(展示帳號) | 手機版 |
|---|---|---|
![]() |
![]() |
![]() |
https://namecallbob.github.io/FoodSelector/
需先在 repo Settings → Pages → Source 選擇「GitHub Actions」,推送到
main後由.github/workflows/deploy-pages.yml自動建置並發佈。
展示為 demo 模式:使用內建種子資料,不需要 PHP 後端即可完整操作。
展示帳號(登入頁可一鍵填入):
| 帳號 | 密碼 | 角色 |
|---|---|---|
binbin |
binbin |
買家 |
teststore1 |
teststore1 |
賣家 |
上述帳密與
frontend/src/data/seed.js、backend/database/seeders/UserSeeder.php中的資料皆為公開的示範資料,不是真實憑證。
「等一下」「隨便」「都可以」是聚餐時最沒有效率的三句話。Google Map 適合找地點,卻不容易一次看到菜色層級的資訊(單品、價格、人氣)。耐人尋味把資料拉到菜色層級,再加上一個「幫我決定」的隨機轉盤,讓選擇困難有一個出口。
| 層 | 技術 |
|---|---|
| 前端 | React 18、Vite 6、React Router 6、Tailwind CSS v4、shadcn 風格自製元件、lucide-react |
| 後端 | PHP 8.1+、Lumen 10、Eloquent、tymon/jwt-auth 2(JWT)、MySQL |
| 測試 | Vitest + jsdom(前端)、PHPUnit 10(後端) |
| CI / 部署 | GitHub Actions → GitHub Pages(前端 demo 建置) |
.
├── backend/ # Lumen (PHP) JWT API
│ ├── app/Http/Controllers # 商品、店家、收藏、隨機、認證
│ ├── app/Http/Middleware # CorsMiddleware / AuthMiddleware(JWT + RBAC)
│ ├── app/Models # Eloquent 模型(private / store / product / collect …)
│ ├── database/ # migrations + seeders(含 RBAC 權限種子)
│ ├── routes/web.php # 路由;受保護路由掛 check.permission:<action>
│ └── tests/ # PHPUnit
├── frontend/ # React + Vite SPA
│ ├── src/lib/api.js # ★ 資料轉接層:demo(種子)/ live(Lumen API)
│ ├── src/data/seed.js # demo 模式的種子資料
│ ├── src/pages/ # 首頁 / 探索 / 店家 / 隨機 / 口袋名單 / 登入
│ └── src/components/ # UI kit、卡片、版面
└── .github/workflows/ # GitHub Pages 部署
關鍵設計 — 單一資料轉接層: frontend/src/lib/api.js 是唯一知道「資料從哪來」的檔案。
VITE_API_MODE=demo 時直接讀取種子資料(可離線、可上 GitHub Pages);VITE_API_MODE=live 時改打 Lumen API。
兩條分支回傳相同的資料形狀(連後端「單一商品回傳一元素陣列」這種怪癖也一併模擬),所以頁面元件完全不需要知道目前是哪個模式。
請求流程(live 模式):
Browser ──JWT──> CorsMiddleware ─> AuthMiddleware ─> Controller ─> Eloquent ─> MySQL
│
├─ token 無效 → 401
└─ 角色缺少該權限 → 403
- 首頁:招牌 Hero、美食分類、今日推薦店家、人氣必吃。
- 探索:關鍵字搜尋、分類與價格篩選,可切換「菜色 / 店家」。
- 店家頁:介紹、地址/營業時間、標籤、菜單、食客評論,並可「幫我從這家挑一道」。
- 隨機挑選:轉盤式動畫,隨機抽一家店或一道菜,戰勝選擇困難。
- 口袋名單:以 localStorage 收藏喜歡的菜色。
- 響應式(手機底部導覽)、鍵盤可操作、
prefers-reduced-motion友善。
以 UI/UX 設計系統評估後採「Vibrant & Block-based」風格:開胃紅 #DC2626 + 珊瑚 #F87171 + 暖金 #A16207;字體 Playfair Display(標題)/ Karla(內文),營造餐飲質感。
需求:Node.js 20+
cd frontend
npm install
npm run dev # http://localhost:5173若要串接真實後端:
cp .env.example .env
# 編輯 frontend/.env
# VITE_API_MODE=live
# VITE_API_BASE=http://localhost:8000需求:PHP 8.1+、Composer、MySQL
cd backend
composer install
cp .env.example .env # 填入 DB 連線資訊
php artisan jwt:secret # 產生 JWT_SECRET(不要提交 .env)
php artisan migrate --seed
php -S localhost:8000 -t public種子資料會建立示範帳號、店家、商品與 RBAC 權限(buyer / seller 角色)。
cd frontend
npm install
npm test # vitest run,39 個測試涵蓋:資料轉接層 demo 分支(搜尋/分類/價格篩選、下架商品不外流、後端形狀模擬、隨機挑選)、
demo 登入流程(錯誤憑證回 401、token 與使用者持久化、登出清除)、種子資料完整性(id 不重複、
外鍵都指得到、每家店都有上架商品)、以及 cn() / formatTWD() 工具函式。
cd backend
composer install
./vendor/bin/phpunittests/RoutesTest.php 檢查路由表:公開路由都有註冊(包含曾經壞掉的 /token/verify/、/store/search),
每個特權路由都掛上正確的 check.permission:<action> middleware,而公開路由不會誤掛權限。
三種方式,任選其一:
- 直接看截圖 — 上方「產品畫面」區塊。
- 本機跑 demo —
cd frontend && npm install && npm run dev(不需要後端)。 - 本機跑正式建置 —
npm run build && npm run preview -- --base /FoodSelector/(正式建置的base是/FoodSelector/,預覽時要帶上同樣的 base)。
老框架僅做「優化與補漏」,重點修正:
- 修補權限中介層的驗證繞過(無效 token 曾被當成通過),正確回傳 401 / 403。
- 修正壞掉的路由(
/token/verify/、/store/search)。 - 補上缺少的
collect資料表 migration 與config/jwt.php。 - 修正
CollectController@read未回傳、ProductController硬編分類/狀態、帳號鎖定字串比對等邏輯漏洞。 - 重寫種子讓 RBAC 真正可用(原本每個受權限保護的路由都會 403)。
- 修正前端 live 模式登入時少了斜線的 API 路徑(
login→/login)。
backend/.env與frontend/.env皆已列入.gitignore,不會被提交。- 兩邊都提供
.env.example作為範本;JWT_SECRET請用php artisan jwt:secret自行產生。 - repo 內出現的帳密(
binbin/binbin等)全部來自種子/示範資料,不是真實憑證。
推送到 main 後由 .github/workflows/deploy-pages.yml 自動建置 frontend/(demo 模式)並發佈到 GitHub Pages。首次需於 repo Settings → Pages → Source 選擇「GitHub Actions」。
後端衍生自 Laravel Lumen 骨架,該骨架採 MIT 授權(見 backend/composer.json)。本 repo 目前尚未附上 LICENSE 檔案。





