feat: enhanced API and interface

This commit is contained in:
2026-01-01 19:41:01 +08:00
parent e657fd1be5
commit 0417b856f3
6 changed files with 557 additions and 60 deletions

View File

@@ -7,19 +7,47 @@
<title>歷史上的今天</title>
<link rel="stylesheet" href="static/styles.css">
<script src="static/script.js"></script>
</head>
<body>
<div class="container">
<h1 id="title">歷史上的今天</h1>
<div id="current-date"></div>
<div class="date-controls">
<button id="prev-btn" class="nav-btn" aria-label="往前一天">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<div id="current-date" title="更改日期"></div>
<button id="next-btn" class="nav-btn" aria-label="往後一天">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<div id="events-list">
<p>正在載入歷史事件...</p>
</div>
<div id="time-comment" class="hide">*以上標註時間均為臺灣標準時間(UTC+8)</div>
<div id="time-comment" class="hide comment">*以上標註時間均為臺灣標準時間(UTC+8)</div>
<div id="help-us" class="comment"><a href="https://forms.gle/jaCTUmfp14nsga1U8"
target="_blank">少了什麼嗎?幫助我們充實資料庫!</a></div>
</div>
<dialog id="date-dialog">
<div class="dialog-content">
<h3>日期</h3>
<input type="date" id="date-picker-input">
<div class="dialog-actions">
<button id="cancel-date-btn" class="cancel-btn">取消</button>
<button id="confirm-date-btn" class="confirm-btn">確認</button>
</div>
</div>
</dialog>
</body>
</html>

View File

@@ -1,21 +0,0 @@
const submit_btn = document.getElementById('submit');
const title_obj = document.getElementById('title');
const desc_obj = document.getElementById('desc');
const date_obj = document.getElementById('date');
const messenger = document.getElementById('messenger');
document.addEventListener('DOMContentLoaded')
submit_btn.addEventListener(onclick, () => {
var title = title_obj.value;
var desc = desc_obj.value;
var date_str = date_obj.value;
if (title == null || desc == null || date == null) {
messenger.innerHTML = '請填寫所有欄位';
messenger.classList.remove("hide");
return;
}
const date = new Date(date_str);
});

View File

@@ -2,48 +2,157 @@ document.addEventListener('DOMContentLoaded', () => {
const dateElement = document.getElementById('current-date');
const eventsList = document.getElementById('events-list');
const timeCommentElement = document.getElementById('time-comment');
const today = new Date();
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
// Date display
const options = { year: 'numeric', month: 'long', day: 'numeric' };
dateElement.textContent = today.toLocaleDateString('zh-TW', options);
// Query to API
fetch('/api/query')
.then(response => response.json())
.then(events => {
eventsList.innerHTML = '';
const dialog = document.getElementById('date-dialog');
const datePickerInput = document.getElementById('date-picker-input');
const cancelDateBtn = document.getElementById('cancel-date-btn');
const confirmDateBtn = document.getElementById('confirm-date-btn');
if (events == null || events.length == 0 || events.length == undefined) {
eventsList.innerHTML = '<p>今天沒有已記載的重大事件。</p>';
timeCommentElement.classList.add('hide');
return;
}
events.forEach(event => {
const card = document.createElement('div');
card.className = 'event-card';
let currentDate;
const urlParams = new URLSearchParams(window.location.search);
const yearParam = urlParams.get("year");
const monthParam = urlParams.get("month");
const dayParam = urlParams.get("day");
const title = document.createElement('div');
title.className = 'event-title';
title.textContent = event.title;
const year = document.createElement('span');
year.className = 'event-year';
year.textContent = `(${event.year} 年)`;
if (yearParam && monthParam && dayParam) {
currentDate = new Date(parseInt(yearParam), parseInt(monthParam) - 1, parseInt(dayParam));
} else {
currentDate = new Date();
}
const description = document.createElement('p');
description.textContent = event.description;
title.appendChild(year);
card.appendChild(title);
card.appendChild(description);
eventsList.appendChild(card);
timeCommentElement.classList.remove('hide');
fetchAndRender(currentDate);
function fetchAndRender(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const newUrl = `?year=${year}&month=${month}&day=${day}`;
window.history.pushState({ path: newUrl }, '', newUrl);
const options = { year: 'numeric', month: 'long', day: 'numeric' };
dateElement.textContent = date.toLocaleDateString('zh-TW', options);
eventsList.innerHTML = '<p>正在載入歷史事件...</p>';
timeCommentElement.classList.add('hide');
const requestURL = `/api/query?year=${year}&month=${month}&day=${day}`;
fetch(requestURL)
.then(response => {
if (!response.ok) throw new Error("Network response was not ok");
return response.json();
})
.then(data => {
renderEvents(data.events);
})
.catch(error => {
console.error('Fetch error:', error);
eventsList.innerHTML = '<p style="color: red;">載入歷史事件失敗。請檢查 API 連線。</p>';
});
})
.catch(error => {
console.error('Fetch error:', error);
eventsList.innerHTML = '<p style="color: red;">載入歷史事件失敗。請檢查 API。</p>';
}
function renderEvents(events) {
eventsList.innerHTML = '';
if (!events || events.length === 0) {
eventsList.innerHTML = '<p>這一天沒有已記載的重大事件。</p>';
timeCommentElement.classList.add('hide');
return;
}
events.forEach(event => {
const card = document.createElement('div');
card.className = 'event-card';
const title = document.createElement('div');
title.className = 'event-title';
title.textContent = event.title;
const yearSpan = document.createElement('span');
yearSpan.className = 'event-year';
yearSpan.textContent = `(${event.year} 年)`;
const description = document.createElement('p');
description.textContent = event.description;
title.appendChild(yearSpan);
card.appendChild(title);
card.appendChild(description);
eventsList.appendChild(card);
});
timeCommentElement.classList.remove('hide');
}
dateElement.addEventListener("wheel", (event) => {
if (event.deltaY > 0) {
nextBtn.click();
} else if (event.deltaY < 0) {
prevBtn.click();
}
})
prevBtn.addEventListener('click', () => {
currentDate.setDate(currentDate.getDate() - 1);
fetchAndRender(currentDate);
});
nextBtn.addEventListener('click', () => {
currentDate.setDate(currentDate.getDate() + 1);
fetchAndRender(currentDate);
});
dateElement.addEventListener('click', () => {
const yyyy = currentDate.getFullYear();
const mm = String(currentDate.getMonth() + 1).padStart(2, '0');
const dd = String(currentDate.getDate()).padStart(2, '0');
datePickerInput.value = `${yyyy}-${mm}-${dd}`;
dialog.showModal();
});
cancelDateBtn.addEventListener('click', () => {
dialog.close();
});
confirmDateBtn.addEventListener('click', () => {
const selectedValue = datePickerInput.value;
if (selectedValue) {
const [y, m, d] = selectedValue.split('-').map(Number);
currentDate = new Date(y, m - 1, d);
fetchAndRender(currentDate);
}
dialog.close();
});
dialog.addEventListener('click', (event) => {
const rect = dialog.getBoundingClientRect();
const isInDialog = (rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX && event.clientX <= rect.left + rect.width);
if (!isInDialog) {
dialog.close();
}
});
dialog.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
event.preventDefault();
confirmDateBtn.click();
}
})
});

View File

@@ -99,9 +99,179 @@ input[type='date'] {
display: none;
}
#time-comment {
.comment {
font-size: 11px;
color: #777;
margin-top: 25px;
margin-bottom: 0;
}
.comment>a {
color: inherit;
text-decoration: inherit;
}
.comment>a::after {
position: absolute;
width: 100%;
transform: scaleX(0);
height: 2px;
bottom: 0;
left: 0;
color: inherit;
transform-origin: bottom right;
transition: transform 0.25s ease-in-out;
}
.comment>a:hover {
text-decoration: underline;
}
.comment>a:hover::after {
transform: scaleX(1);
transform-origin: bottom left;
}
dialog {
border: none;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
padding: 0;
width: 60%;
max-width: 400px;
animation: fadeIn 0.3s ease-out;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
}
.dialog-content {
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
width: 80%;
justify-self: center;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
.dialog-content h3 {
margin: 0;
color: #333;
font-size: 1.2em;
}
input[type='date'][id='date-picker-input'] {
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
outline: none;
font-family: inherit;
margin-left: auto;
margin-right: auto;
width: 85%;
}
input[type='date']:focus {
border-color: #ff69b4;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
button.confirm-btn,
button.cancel-btn {
padding: 8px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 0.9em;
transition: background 0.2s;
}
button.confirm-btn {
background-color: #ff69b4;
color: white;
}
button.confirm-btn:hover {
background-color: #e0559a;
}
button.cancel-btn {
background-color: #f0f0f0;
color: #555;
}
button.cancel-btn:hover {
background-color: #e0e0e0;
}
.nav-btn {
background: none;
border: none;
cursor: pointer;
color: #555;
padding: 10px;
border-radius: 50%;
transition: background-color 0.2s, color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.nav-btn:hover {
background-color: #ffe6f2;
color: #ff69b4;
}
.date-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
#current-date {
margin-bottom: 0;
cursor: pointer;
padding: 5px 15px;
border-radius: 20px;
transition: background-color 0.2s;
font-weight: bold;
user-select: none;
font-size: 100%;
}
#current-date:hover {
background-color: #f0f0f0;
color: #ff69b4;
}
/* #current-date::after {
content: ' 📅';
font-size: 0.8em;
opacity: 0.5;
} */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}