MyCdA
Free CdA calculator for cyclists: measure your drag area (CdA) and rolling resistance (Crr) from power meter field tests with Chung's virtual elevation method.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Web App (PWA) ธีม Sporty Neon-Lime & Sleek Graphite-Black สำหรับจดสมาชิก นับลูก คำนวณค่าใช้จ่าย และจัดคิวเกมแบดมินตัน Real-time sync ทุกคนเห็นพร้อมกัน — ใช้ฟรี 100%
เวอร์ชันปัจจุบัน: 3.19.0 (cache badcount-v55)
🌐 Live: badcount.vercel.app
📦 GitHub: leezen441/Badcount
#b5f714) และดำกราไฟต์ (#0d0e12) — initTheme() ใส่คลาส dark ตลอด ไม่มีโหมดสว่าง#121315) บนปุ่มเขียวนีออน อ่านง่ายภายใต้แสงสนามsettings/defaults (lastUsedCourts)1-5, 8) หรือโหมดนับจำนวนอย่างง่าย (simpleShuttleCount) — ลูกจากแมตช์หารให้ผู้เล่นในเกมนั้น (ยกเว้นคนที่ถูก exempt)manualFee ให้แอดมินใส่ยอดคงที่รายคนrounded-2xlbadcount-v33 ใน sw.js — เคลียร์แคชเก่าเมื่ออัปเดตpremium.css?v=3.16.0 และ app.js?v=3.16.0 ใน index.html
app.js / premium.css ต้อง bump ?v= ใน index.html และ CACHE_NAME ใน sw.js ไม่งั้นผู้ใช้ไม่เห็นของใหม่settings/defaults (canonical EMV + quiet zone) — fallback เป็น session.bankQR ถ้ายังไม่มี PromptPay IDsessions/{id}/receipts/{memberId} เป็น base64 ใน Firestore (ไม่ใช้ Firebase Storage) พร้อม metadata ลบอัตโนมัติ ~30 วันPASSCODE_HASH ใน app.js — จำใน localStorage 30 วันapp.js (ข้อความตรง ไม่ได้ hash) — จำจนกว่าจะออกจากระบบ/#/m/{id}isPaused: true จนแอดมินปลดเมื่อมาถึงสนามHash router ใน app.js (route()):
| URL | สิทธิ์ | หน้าที่ |
|---|---|---|
/#/ | Admin | แดชบอร์ด สร้าง/เปิดก๊วน |
/#/history | Admin (Manager ถูกส่งไป m-home) | ประวัติกลุ่ม 50 รายการล่าสุด |
/#/personal-stats | Admin & Manager | สถิติรายคน จำนวนเกม ค่าลูก ยอดเงิน ตามช่วงเวลา |
/#/admin-summary | Admin & Manager | สรุปยอดรับ จ่าย ค้าง |
/#/session/{id} | Admin (ไม่ล็อกอินจะล็อก nav) | จัดการก๊วนเต็มสิทธิ์: สมาชิก สนาม เงิน จัดเกมนับลูก |
/#/m-home | Manager / Admin | รายการก๊วนล่าสุดสำหรับผู้จัดการ |
/#/m/{id} | Temp Manager | จัดการก๊วนนี้ (ต้อง PIN 4 หลักของก๊วนนั้น ถ้ายังไม่ผ่าน) |
/#/join/{id} | Public | ลงชื่อ (พักคิวอัตโนมัติ · จำระดับมือ) ดูยอด อัปโหลดสลิป — เลือก buddy ได้เมื่อก๊วนเป็นโหมด advance |
บอท LINE Official Account ผ่าน Vercel Serverless Functions (api/) ต่อ Firestore โปรเจกต์เดียวกัน (badcount-a1296)
| พิมพ์ในแชต/กลุ่ม | ใครสั่งได้ | การทำงาน |
|---|---|---|
startbadcount | เจ้าของ | เริ่มทำงาน + จำแชต/กลุ่มนี้เป็นปลายทาง · คนแรกที่พิมพ์ = เจ้าของถาวร (Trust-On-First-Use) |
stopbadcount | เจ้าของ | หยุดทำงาน — คำสั่ง invite / close จะเงียบด้วย |
invite | ทุกคน | ตอบ list invite ของก๊วนที่เปิดอยู่ล่าสุด (ไม่มีก๊วนเปิด → แจ้งว่าไม่มี) |
close | ทุกคน / คนที่อนุญาต | ก๊วนล่าสุดที่วันที่ไม่เกินวันนี้ (ข้ามก๊วนล่วงหน้า) · ปิดแล้ว → ทุกคนได้ list จ่ายเงิน · ยังเปิด → เจ้าของหรือคนที่อนุญาต = ปิด Court ในแอปให้เลย + ตอบ list จ่ายเงิน · คนที่ยังไม่อนุญาต = ได้รหัส 4 หลักให้เจ้าของอนุมัติ |
allow <รหัส> | เจ้าของ | อนุญาตคนที่ขอรหัสนั้นให้ปิด Court ผ่าน close ได้ถาวร (รหัสหมดอายุ 30 นาที) |
closers | เจ้าของ | ดูรายชื่อคนที่ปิด Court ได้ |
disallow <ลำดับ> | เจ้าของ | ถอนสิทธิ์คนลำดับนั้นจาก closers |
Close, INVITE)invite / close ใช้ได้เฉพาะตอนบอท activesettings/lineBot.closers (เปลี่ยนชื่อในไลน์ก็ปลอมไม่ได้)startbadcountindex.html ข้างช่องวันที่) — handler ใน app.js และ api/line-notify.js ยังอยู่ครบ ลบคอมเมนต์ออกก็ใช้ได้ทันทีremindUnpaidViaLine() ใน app.js ยังเก็บไว้ ผูกกลับที่ listener ของ btnRemindUnpaid เพื่อใช้ใหม่LINE_CHANNEL_ACCESS_TOKEN, LINE_CHANNEL_SECRET (ตัวเลือก PUBLIC_BASE_URL ถ้าต้องการบังคับโดเมน)https://<โดเมน>/api/line-webhook → เปิด Use webhookstartbadcount (คนพิมพ์คนแรก = เจ้าของ)LINE API รันได้เมื่อ deploy บน Vercel (หรือ vercel dev) — สแตติกเซิร์ฟเวอร์อย่าง python -m http.server จะไม่รัน /api/*
settings/lineBot — adminUserId, active, groupId / roomId / directUserId, updatedAtlineNotifiedAt, lineOpenNotifiedAt (optional) — ไม่แตะ members / ค่าใช้จ่าย / สลิปบอทผูกกับ Firebase project ของ instance นั้น — ถ้าจะทำ Team B ต้องสร้าง channel + ตั้ง env แยก และชี้ firebase config ใน
api/_firebase.jsไปโปรเจกต์นั้น
Badcount/
├─ index.html ← UI หลัก (premium.css?v=3.16.0 / app.js?v=3.16.0)
├─ premium.css ← ธีมเขียวนีออน-ดำกราไฟต์
├─ app.js ← Logic หลักทั้งหมด (จัดคิว, คำนวณเงิน, PWA, join, LINE ฝั่งไคลเอนต์)
├─ firebase-config.js ← Firestore web config (โปรเจกต์ badcount-a1296)
├─ manifest.json ← PWA manifest
├─ icon.svg / icon-maskable.svg
├─ sw.js ← Service Worker (CACHE_NAME = badcount-v55)
├─ qrcode.min.js ← สร้าง PromptPay QR (local)
├─ package.json ← dependency `firebase` สำหรับ serverless เท่านั้น
├─ api/ ← LINE Bot — Vercel Serverless Functions
│ ├─ line-webhook.js ← startbadcount / stopbadcount
│ ├─ line-notify.js ← push invite / ทวงเงิน (เรียกจากปุ่มเท่านั้น)
│ ├─ _totals.js ← สูตรเงิน (ต้องตรงกับ calcSessionTotals ใน app.js)
│ └─ _firebase.js / _line.js / _sessions.js / _notify.js
├─ .gitignore
└─ README.md
ไม่มี bundler / TypeScript / ชุดทดสอบ — เปิด index.html คุยกับ Firestore โดยตรง
badcount</> (Web App)badcount-web → กด Register appfirebaseConfig ไปใส่ใน firebase-config.jsแอปนี้ ไม่ใช้ Firebase Authentication — ล็อกอินเป็น passcode ฝั่งไคลเอนต์ ไม่ต้องเปิด Auth
asia-southeast3 (Bangkok) (เปลี่ยนทีหลังไม่ได้)ใน Firestore → แท็บ Rules — rules ชุดที่ใช้อยู่เปิดอ่าน/เขียนได้ทุกคน (จำกัดขนาดสมาชิก ≤ 50):
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /sessions/{sessionId} {
allow read: if true;
allow create: if request.resource.data.keys().hasAll(['date', 'members', 'status'])
&& request.resource.data.members.size() <= 50;
allow update: if request.resource.data.members.size() <= 50;
allow delete: if true;
match /receipts/{memberId} {
allow read: if true;
allow write: if true;
}
}
match /settings/{docId} {
allow read: if true;
allow write: if true;
}
}
}
กด Publish
ผู้ที่มีลิงก์ก๊วนหรือ join URL อ่าน/แก้ข้อมูลได้ตาม rules ด้านบน — แชร์เฉพาะกลุ่มที่ไว้ใจ
firebase-config.jsแก้ไฟล์ firebase-config.js ในโฟลเดอร์โปรเจกต์:
export const firebaseConfig = {
apiKey: "AIza...",
authDomain: "your-project.firebaseapp.com",
projectId: "your-project",
storageBucket: "your-project.appspot.com",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc123"
};
ค่าเหล่านี้เป็น public client config ตามที่ Firebase ออกแบบไว้ ความคุมสิทธิ์อยู่ที่ Security Rules
ใน app.js หา PASSCODE_HASH แล้วนำรหัสที่ต้องการมา hash เป็น SHA-256
ใน Console ของเบราว์เซอร์ (F12):
const buf = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("YOUR_PASSWORD")
);
console.log(
Array.from(new Uint8Array(buf))
.map(b => b.toString(16).padStart(2, "0"))
.join("")
);
วางผลลัพธ์ลง PASSCODE_HASH
รหัสผู้ช่วย (MANAGER_PASSCODE) ตั้งแยกในไฟล์เดียวกัน — เป็นข้อความตรง ไม่ได้ hash
รีโปที่ใช้อยู่แล้ว: https://github.com/leezen441/Badcount.git
ถ้าเริ่มรีโปใหม่:
git init
git add .
git commit -m "Initial BadCount App Setup"
git remote add origin https://github.com/USERNAME/badcount.git
git branch -M main
git push -u origin main
/api)https://your-app.vercel.app🎉 ส่งลิงก์ก๊วนให้สมาชิกลงชื่อและสแกนจ่ายได้ทันที
สูตรใน calcSessionTotals (app.js) และ api/_totals.js ต้องตรงกัน:
N = จำนวนสมาชิกในก๊วน
ค่าคอร์ตต่อคน = (ค่าคอร์ตรวม ÷ N) หรือ ราคาต่อคนคงที่ ตาม courtFeeType
ค่าอื่นๆ ต่อคน = (ค่ารวม ÷ N) หรือ ราคาต่อคนคงที่ ตาม otherCostType
ลูกของสมาชิก X =
shuttlesUsed (ใส่เอง) + ส่วนแบ่งลูกจากทุกแมตช์ที่ X ลง
(แมตช์หารให้ผู้เล่นในเกมนั้น; ถ้ามีคน exempt คนที่เหลือแบกส่วนนั้น)
ลูกที่คิดเงิน = 0 ถ้า excludeAllShuttles
หรือ max(0, ลูกของ X − shuttlesExcluded)
หรือตามปกติ
ยอด X = ค่าคอร์ตต่อคน + ค่าอื่นๆ ต่อคน + (ลูกที่คิดเงิน × shuttlePrice)
ถ้ามี manualFee → ใช้ยอดนั้นแทนทั้งสูตร
โหมดลูก:
1-5, 8 แล้วระบบนับจำนวนsimpleShuttleCount): ใส่ตัวเลขจำนวนลูกตรงๆก๊วน 4 คน ค่าคอร์ตรวม 400 · ค่าน้ำรวม 100 · ลูกละ 25 · A ใช้ 2, B 1, C 2, D 1
| สมาชิก | ค่าสนาม | ค่าน้ำ/อื่นๆ | ค่าลูก | ยอดสุทธิ |
|---|---|---|---|---|
| A | 100 ฿ | 25 ฿ | 50 ฿ (2 ลูก) | 175 ฿ |
| B | 100 ฿ | 25 ฿ | 25 ฿ (1 ลูก) | 150 ฿ |
| C | 100 ฿ | 25 ฿ | 50 ฿ (2 ลูก) | 175 ฿ |
| D | 100 ฿ | 25 ฿ | 25 ฿ (1 ลูก) | 150 ฿ |
| รวม | 400 ฿ | 100 ฿ | 150 ฿ | 650 ฿ |
ถ้าเปิดใน LINE in-app browser แอปจะพยายามเด้งออกไปบราวเซอร์จริง (openExternalBrowser=1) เพราะแชร์รูป/กล้องใน LINE มักไม่ครบ
แอปหลัก (สแตติก + Firestore):
python -m http.server 8000
เปิด http://localhost:8000
หรือ VS Code Live Server ที่ index.html
ต้องมีเน็ตสำหรับ Firebase + CDN (Tailwind, Fonts, jsQR)
LINE /api/*: ใช้ vercel dev หลัง npm install และใส่ env ของ LINE — http.server ไม่รัน serverless
| บริการ | Free tier ที่เกี่ยวข้อง | หมายเหตุของแอปนี้ |
|---|---|---|
| Vercel Hosting | bandwidth ตามแพลน Hobby | สแตติก + serverless LINE |
| Firestore (Spark) | reads / writes รายวัน | ฐานข้อมูลหลัก + สลิป base64 ใน subcollection receipts |
| LINE Messaging API | ตามโควตาช่องทาง | เลยตัดการส่งอัตโนมัติทั้งหมด เหลือแค่กดปุ่มเอง |
ไม่ใช้ Firebase Storage — รูปสลิปอยู่ใน Firestore document
| เลเยอร์ | ของที่ใช้ |
|---|---|
| Frontend | HTML + premium.css + Tailwind CDN + Vanilla JS (ไม่มี build) |
| Database | Firebase Firestore realtime (onSnapshot) |
| Hosting | Vercel (push GitHub → deploy) |
| Auth | Admin SHA-256 + Manager passcode + Temp PIN — ไม่ใช้ Firebase Auth |
| PWA | Manifest + Service Worker + Notification API |
| QR | qrcode.min.js (สร้าง PromptPay) + jsQR CDN (อ่านสลิป) |
| LINE | Messaging API ผ่าน api/*.js |
MANAGER_PASSCODE) เป็นข้อความตรงใน app.js — เปลี่ยนเองถ้าแชร์รีโปสาธารณะ/#/session/{id}, /#/m/{id} หรือ /#/join/{id} เข้าถึงข้อมูลก๊วนนั้นได้/api/line-notify ไม่มี auth ของผู้เรียก — ใครรู้ sessionId ตอนบอท active อาจทริกเกอร์โพสต์ LINE ได้โปรเจกต์นี้อยู่ภายใต้สัญญาอนุญาตสิทธิ์ MIT License
นำไปปรับแต่ง แก้ไข แจกจ่าย หรือพัฒนาต่อยอดในก๊วนของคุณได้โดยไม่ต้องขออนุญาต
🏸 BadCount — Count Every Game. Enjoy Every Match.