Download index.html from OuiMCB/Time: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/OuiMCB/Time/resolve/main/index.html
- Command line
-
hf download hf://spaces/OuiMCB/Time/index.html
-
curl -L -o index.html https://huggingface.co/spaces/OuiMCB/Time/resolve/main/index.html
10.3 kB
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>7人共同空檔協調行事曆</title> | |
| <style> | |
| :root { | |
| --primary-color: #4a90e2; | |
| --success-color: #2ecc71; | |
| --bg-color: #f4f7f6; | |
| --card-bg: #ffffff; | |
| --border-color: #e0e0e0; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| background-color: var(--bg-color); | |
| color: #333; | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #2c3e50; | |
| margin-bottom: 10px; | |
| } | |
| .status-indicator { | |
| text-align: center; | |
| font-size: 0.9rem; | |
| color: #666; | |
| margin-bottom: 20px; | |
| height: 20px; | |
| } | |
| /* 頂部交集結果區塊 */ | |
| .intersection-card { | |
| background-color: #e8f8f5; | |
| border: 2px solid var(--success-color); | |
| border-radius: 8px; | |
| padding: 15px 20px; | |
| margin-bottom: 25px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| } | |
| .intersection-card h2 { | |
| margin-top: 0; | |
| color: #27ae60; | |
| font-size: 1.2rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .available-tags { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .tag { | |
| background-color: var(--success-color); | |
| color: white; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| font-size: 0.95rem; | |
| } | |
| .no-match { | |
| color: #7f8c8d; | |
| font-style: italic; | |
| } | |
| /* 行事曆列表 */ | |
| .calendar-grid { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .date-card { | |
| background: var(--card-bg); | |
| border-radius: 8px; | |
| border: 1px solid var(--border-color); | |
| padding: 15px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.02); | |
| transition: all 0.3s ease; | |
| } | |
| .date-card.all-available { | |
| border: 2px solid var(--success-color); | |
| background-color: #f9fffb; | |
| } | |
| .date-header { | |
| font-size: 1.1rem; | |
| font-weight: bold; | |
| color: #34495e; | |
| margin-bottom: 12px; | |
| padding-bottom: 8px; | |
| border-bottom: 1px solid #eee; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .status-badge { | |
| font-size: 0.85rem; | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| background-color: #eee; | |
| color: #666; | |
| } | |
| .date-card.all-available .status-badge { | |
| background-color: var(--success-color); | |
| color: white; | |
| } | |
| /* 七小欄 Grid Layout */ | |
| .members-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); | |
| gap: 10px; | |
| } | |
| .member-col { | |
| background: #f8f9fa; | |
| border: 1px solid #e9ecef; | |
| border-radius: 6px; | |
| padding: 8px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .member-name-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-weight: bold; | |
| color: #495057; | |
| font-size: 0.95rem; | |
| } | |
| .member-col label { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 0.8rem; | |
| color: #6c757d; | |
| cursor: pointer; | |
| } | |
| .member-col input[type="text"] { | |
| width: 100%; | |
| padding: 6px; | |
| border: 1px solid #ced4da; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| box-sizing: border-box; | |
| } | |
| .member-col input[type="text"]:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); | |
| } | |
| @media (max-width: 768px) { | |
| .members-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| </style> | |
| <!-- Firebase SDK --> | |
| <script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-database-compat.js"></script> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>📅 7人空檔時間協調行事曆 (8/14 - 9/2)</h1> | |
| <div id="sync-status" class="status-indicator">📡 連線中...</div> | |
| <!-- 共同有空交集區塊 --> | |
| <div class="intersection-card"> | |
| <h2>🎉 全員皆有空的日期(交集):</h2> | |
| <div id="intersection-results" class="available-tags"> | |
| <span class="no-match">讀取中...</span> | |
| </div> | |
| </div> | |
| <!-- 日期清單區塊 --> | |
| <div id="calendar" class="calendar-grid"></div> | |
| </div> | |
| <script> | |
| // 1. 初始化 Firebase Realtime Database | |
| const firebaseConfig = { | |
| databaseURL: "https://bolobolobolomi-66014-default-rtdb.asia-southeast1.firebasedatabase.app/" | |
| }; | |
| firebase.initializeApp(firebaseConfig); | |
| const db = firebase.database(); | |
| const scheduleRef = db.ref("schedule_2026"); | |
| const members = ["桑", "吳", "張", "趙", "謝", "黃", "康"]; | |
| const startDate = new Date("2026-08-14"); | |
| const endDate = new Date("2026-09-02"); | |
| // 生成日期列表 | |
| const dateList = []; | |
| let curr = new Date(startDate); | |
| while (curr <= endDate) { | |
| const year = curr.getFullYear(); | |
| const month = String(curr.getMonth() + 1).padStart(2, '0'); | |
| const day = String(curr.getDate()).padStart(2, '0'); | |
| const dateStr = `${year}-${month}-${day}`; | |
| const weekDays = ["日", "一", "二", "三", "四", "五", "六"]; | |
| const dayOfWeek = weekDays[curr.getDay()]; | |
| dateList.push({ dateStr, displayStr: `${month}/${day} (${dayOfWeek})` }); | |
| curr.setDate(curr.getDate() + 1); | |
| } | |
| // 2. 渲染 DOM | |
| const calendarContainer = document.getElementById("calendar"); | |
| const syncStatus = document.getElementById("sync-status"); | |
| dateList.forEach(({ dateStr, displayStr }) => { | |
| const dateCard = document.createElement("div"); | |
| dateCard.className = "date-card"; | |
| dateCard.id = `card-${dateStr}`; | |
| let membersHTML = ""; | |
| members.forEach(name => { | |
| membersHTML += ` | |
| <div class="member-col"> | |
| <div class="member-name-row"> | |
| <span>${name}</span> | |
| <label> | |
| <input type="checkbox" id="chk-${dateStr}-${name}" onchange="saveData('${dateStr}', '${name}')"> | |
| 有空 | |
| </label> | |
| </div> | |
| <input type="text" id="txt-${dateStr}-${name}" placeholder="輸入時間..." oninput="saveData('${dateStr}', '${name}')"> | |
| </div> | |
| `; | |
| }); | |
| dateCard.innerHTML = ` | |
| <div class="date-header"> | |
| <span>${displayStr}</span> | |
| <span class="status-badge" id="badge-${dateStr}">計算中...</span> | |
| </div> | |
| <div class="members-grid"> | |
| ${membersHTML} | |
| </div> | |
| `; | |
| calendarContainer.appendChild(dateCard); | |
| }); | |
| // 3. 儲存資料機制(防抖動) | |
| const debounceTimers = {}; | |
| function saveData(dateStr, memberName) { | |
| syncStatus.innerText = "⏳ 儲存中..."; | |
| syncStatus.style.color = "#e67e22"; | |
| const key = `${dateStr}-${memberName}`; | |
| clearTimeout(debounceTimers[key]); | |
| debounceTimers[key] = setTimeout(() => { | |
| const isAvailable = document.getElementById(`chk-${dateStr}-${memberName}`).checked; | |
| const textNote = document.getElementById(`txt-${dateStr}-${memberName}`).value; | |
| scheduleRef.child(dateStr).child(memberName).set({ | |
| available: isAvailable, | |
| note: textNote | |
| }, (error) => { | |
| if (error) { | |
| syncStatus.innerText = "❌ 儲存失敗!請檢查 Firebase 規則"; | |
| syncStatus.style.color = "#e74c3c"; | |
| console.error("Firebase 保存失敗:", error); | |
| } else { | |
| syncStatus.innerText = "✅ 所有變更已即時同步保存"; | |
| syncStatus.style.color = "#27ae60"; | |
| } | |
| }); | |
| }, 300); | |
| } | |
| // 4. 即時監聽與畫面更新 | |
| scheduleRef.on("value", (snapshot) => { | |
| syncStatus.innerText = "✅ 資料庫已連線,即時同步中"; | |
| syncStatus.style.color = "#27ae60"; | |
| const data = snapshot.val() || {}; | |
| const fullAvailableDates = []; | |
| dateList.forEach(({ dateStr, displayStr }) => { | |
| const dateData = data[dateStr] || {}; | |
| let availableCount = 0; | |
| members.forEach(name => { | |
| const memberData = dateData[name] || { available: false, note: "" }; | |
| const chkEl = document.getElementById(`chk-${dateStr}-${name}`); | |
| const txtEl = document.getElementById(`txt-${dateStr}-${name}`); | |
| // 若使用者當前正在輸入該格,不打斷輸入 | |
| if (document.activeElement !== txtEl) { | |
| txtEl.value = memberData.note || ""; | |
| } | |
| chkEl.checked = !!memberData.available; | |
| if (memberData.available) { | |
| availableCount++; | |
| } | |
| }); | |
| const cardEl = document.getElementById(`card-${dateStr}`); | |
| const badgeEl = document.getElementById(`badge-${dateStr}`); | |
| if (availableCount === members.length) { | |
| cardEl.classList.add("all-available"); | |
| badgeEl.innerText = "🎉 全員皆有空!"; | |
| fullAvailableDates.push(displayStr); | |
| } else { | |
| cardEl.classList.remove("all-available"); | |
| badgeEl.innerText = `有空人數:${availableCount}/${members.length}`; | |
| } | |
| }); | |
| // 更新頂部交集 | |
| const resultsContainer = document.getElementById("intersection-results"); | |
| if (fullAvailableDates.length > 0) { | |
| resultsContainer.innerHTML = fullAvailableDates | |
| .map(d => `<span class="tag">${d}</span>`) | |
| .join(""); | |
| } else { | |
| resultsContainer.innerHTML = `<span class="no-match">目前尚無所有人都有空的日期</span>`; | |
| } | |
| }, (error) => { | |
| syncStatus.innerText = "❌ 無法讀取資料,請至 Firebase 設定 Rules 權限!"; | |
| syncStatus.style.color = "#e74c3c"; | |
| console.error("Firebase 讀取失敗:", error); | |
| }); | |
| </script> | |
| </body> | |
| </html> |