tudiendialy / index.html
CVNSS's picture
Update index.html
bff1463 verified
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Từ Điển Thuật Ngữ Địa Lý</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* --- CSS VARIABLES & RESET --- */
:root {
--primary: #0ea5e9; /* Sky Blue */
--primary-dark: #0284c7;
--bg-body: #f8fafc;
--bg-card: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; }
body {
background-color: var(--bg-body);
color: var(--text-main);
line-height: 1.6;
padding-bottom: 40px;
}
/* --- LAYOUT --- */
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
/* --- HEADER & SEARCH --- */
header {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: white;
padding: 60px 0 40px;
text-align: center;
margin-bottom: 30px;
box-shadow: var(--shadow-md);
}
h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 10px;
letter-spacing: -0.025em;
}
p.subtitle {
color: #94a3b8;
font-size: 0.95rem;
margin-bottom: 30px;
}
.search-box {
position: relative;
max-width: 600px;
margin: 0 auto;
}
.search-input {
width: 100%;
padding: 16px 24px 16px 50px;
border-radius: 50px;
border: none;
font-size: 1.1rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
outline: none;
transition: all 0.3s ease;
}
.search-input:focus {
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.3);
}
.search-icon {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
width: 20px;
height: 20px;
}
/* --- RESULTS LIST --- */
#results-area {
display: grid;
gap: 20px;
}
.term-card {
background: var(--bg-card);
padding: 24px;
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: var(--shadow-sm);
transition: transform 0.2s ease, box-shadow 0.2s ease;
animation: fadeIn 0.4s ease-out;
}
.term-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--primary);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 10px;
}
.term-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-dark);
}
.term-type {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
background-color: #f1f5f9;
color: var(--text-muted);
padding: 4px 10px;
border-radius: 6px;
font-weight: 600;
}
.term-def {
color: #334155;
font-size: 1rem;
text-align: justify;
}
/* --- STATES --- */
.loading, .error, .empty {
text-align: center;
padding: 40px;
color: var(--text-muted);
}
.error { color: #ef4444; }
.highlight {
background-color: #fef08a; /* Yellow highlight */
padding: 0 2px;
border-radius: 2px;
}
/* --- ANIMATION --- */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- RESPONSIVE --- */
@media (max-width: 600px) {
h1 { font-size: 1.8rem; }
.container { padding: 0 15px; }
header { padding: 40px 0 30px; border-radius: 0 0 20px 20px; }
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Sổ Tay Thuật Ngữ Địa Lý</h1>
<p class="subtitle">Tra cứu nhanh chóng, chính xác và tiện lợi</p>
<div class="search-box">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input type="text" id="searchInput" class="search-input" placeholder="Nhập từ khoá (ví dụ: Bazan, Thủy triều, Khí hậu...)" autocomplete="off">
</div>
</div>
</header>
<main class="container">
<div id="count-display" style="margin-bottom: 15px; color: var(--text-muted); font-size: 0.9rem;"></div>
<div id="results-area">
</div>
</main>
<script>
/**
* LOGIC ỨNG DỤNG TRA CỨU
* Chuyên gia: AI Assistant (100 Years Exp Sim)
*/
const resultsArea = document.getElementById('results-area');
const searchInput = document.getElementById('searchInput');
const countDisplay = document.getElementById('count-display');
let dictionaryData = [];
// 1. Hàm khởi tạo: Tải dữ liệu JSON
async function initApp() {
renderState('loading', 'Đang tải dữ liệu từ điển...');
try {
// Lưu ý: Cần chạy qua Live Server hoặc Local Server để tránh lỗi CORS khi fetch file local
const response = await fetch('tudien_dialy.json');
if (!response.ok) throw new Error("Không tìm thấy file tudien_dialy.json");
dictionaryData = await response.json();
// Sắp xếp theo bảng chữ cái
dictionaryData.sort((a, b) => a.tu_vung.localeCompare(b.tu_vung, 'vi'));
renderData(dictionaryData); // Hiển thị toàn bộ ban đầu
countDisplay.textContent = `Tổng số thuật ngữ: ${dictionaryData.length}`;
} catch (error) {
console.error(error);
renderState('error', `
<strong>Lỗi tải dữ liệu!</strong><br>
1. Hãy chắc chắn file "tudien_dialy.json" nằm cùng thư mục.<br>
2. Nếu mở trực tiếp file HTML, trình duyệt có thể chặn (CORS).<br>
Hãy dùng VS Code "Live Server" hoặc upload lên host.
`);
}
}
// 2. Hàm xử lý hiển thị (Render)
function renderData(data, keyword = '') {
resultsArea.innerHTML = '';
if (data.length === 0) {
renderState('empty', 'Không tìm thấy thuật ngữ nào phù hợp.');
return;
}
// Sử dụng DocumentFragment để tối ưu hiệu năng render
const fragment = document.createDocumentFragment();
data.forEach(item => {
const card = document.createElement('div');
card.className = 'term-card';
// Highlight từ khóa nếu đang tìm kiếm
let displayTitle = item.tu_vung;
if (keyword) {
const regex = new RegExp(`(${keyword})`, 'gi');
displayTitle = item.tu_vung.replace(regex, '<span class="highlight">$1</span>');
}
card.innerHTML = `
<div class="card-header">
<div class="term-title">${displayTitle}</div>
<span class="term-type">${item.phan_loai || 'Thuật ngữ'}</span>
</div>
<div class="term-def">
${item.nghia}
</div>
`;
fragment.appendChild(card);
});
resultsArea.appendChild(fragment);
// Cập nhật số lượng
if(keyword) {
countDisplay.textContent = `Tìm thấy ${data.length} kết quả cho "${keyword}"`;
} else {
countDisplay.textContent = `Tổng số thuật ngữ: ${dictionaryData.length}`;
}
}
// 3. Hàm hiển thị trạng thái (Loading/Error)
function renderState(type, message) {
resultsArea.innerHTML = `<div class="${type}">${message}</div>`;
}
// 4. Hàm chuẩn hóa tiếng Việt để tìm kiếm thông minh
// Ví dụ: Nhập "khi hau" vẫn tìm ra "Khí hậu"
function removeVietnameseTones(str) {
str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g,"a");
str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g,"e");
str = str.replace(/ì|í|ị|ỉ|ĩ/g,"i");
str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g,"o");
str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g,"u");
str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g,"y");
str = str.replace(/đ/g,"d");
str = str.replace(/À|Á|Ạ|Ả|Ã|Â|Ầ|Ấ|Ậ|Ẩ|Ẫ|Ă|Ằ|Ắ|Ặ|Ẳ|Ẵ/g, "A");
str = str.replace(/È|É|Ẹ|Ẻ|Ẽ|Ê|Ề|Ế|Ệ|Ể|Ễ/g, "E");
str = str.replace(/Ì|Í|Ị|Ỉ|Ĩ/g, "I");
str = str.replace(/Ò|Ó|Ọ|Ỏ|Õ|Ô|Ồ|Ố|Ộ|Ổ|Ỗ|Ơ|Ờ|Ớ|Ợ|Ở|Ỡ/g, "O");
str = str.replace(/Ù|Ú|Ụ|Ủ|Ũ|Ư|Ừ|Ứ|Ự|Ử|Ữ/g, "U");
str = str.replace(/Ỳ|Ý|Ỵ|Ỷ|Ỹ/g, "Y");
str = str.replace(/Đ/g, "D");
// Một số ký tự đặc biệt
str = str.replace(/\u0300|\u0301|\u0303|\u0309|\u0323/g, "");
return str;
}
// 5. Sự kiện tìm kiếm (Debounce nhẹ để mượt mà)
let timeout = null;
searchInput.addEventListener('input', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const rawKeyword = e.target.value.trim();
const normalizeKeyword = removeVietnameseTones(rawKeyword).toLowerCase();
if (!rawKeyword) {
renderData(dictionaryData);
return;
}
// Lọc dữ liệu: So sánh cả từ gốc và từ đã bỏ dấu
const filtered = dictionaryData.filter(item => {
const rawTerm = item.tu_vung.toLowerCase();
const normTerm = removeVietnameseTones(item.tu_vung).toLowerCase();
const rawDef = item.nghia.toLowerCase();
const normDef = removeVietnameseTones(item.nghia).toLowerCase();
return rawTerm.includes(rawKeyword.toLowerCase()) ||
normTerm.includes(normalizeKeyword) ||
rawDef.includes(rawKeyword.toLowerCase()) ||
normDef.includes(normalizeKeyword);
});
renderData(filtered, rawKeyword);
}, 300); // Đợi 300ms sau khi ngừng gõ mới tìm
});
// Khởi chạy ứng dụng
initApp();
</script>
</body>
</html>