Sách Giáo Khoa Việt Nam - sachgiaokhoa.org

CodeIgniter 4 website:
- Catalog 999 SGK (lớp 1-12) từ API, lọc theo lớp/môn/NXB, tìm kiếm
- Trang chủ grouped browse (bậc học -> lớp -> môn, ưu tiên môn quan trọng)
- Chi tiết sách: metadata đầy đủ, cover nghệ thuật /thumb/{W}x{H}/
- Đọc online qua gate captcha + countdown, PDF viewer pdf.js
- Download: captcha + token một lần + auto cache-bust version
- Tự đồng bộ catalog/cover khi API thay đổi (refresh:catalog)
- Legal: DMCA, miễn trừ trách nhiệm, điều khoản, bảo mật, liên hệ
This commit is contained in:
sachgiaokhoaorg
2026-08-23 07:05:49 +00:00
commit e6b265db99
494 changed files with 134829 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<div class="hero">
<h1>Sách Giáo Khoa Việt Nam — Lớp 1 đến lớp 12</h1>
<p>Kho tra cứu đầy đủ <?= number_format($total) ?> đầu sách giáo khoa thuộc mọi bộ sách: Kết nối tri thức với cuộc sống, Chân trời sáng tạo, Cánh Diều, Giáo dục Việt Nam... Chọn lớp hoặc môn học để bắt đầu.</p>
<div class="stats">
<div class="stat"><b><?= number_format($total) ?></b><span>Đầu sách</span></div>
<div class="stat"><b>12</b><span>Khoảng lớp</span></div>
<div class="stat"><b><?= count($publishers) ?></b><span>Nhà xuất bản / bộ sách</span></div>
</div>
</div>
<?php foreach ($levels as $lv): ?>
<section class="level-section">
<h2 class="level-title"><?= esc($lv['label']) ?></h2>
<div class="grade-grid">
<?php foreach ($lv['grades'] as $g): ?>
<div class="grade-card">
<div class="grade-covers">
<?php foreach (array_slice($g['covers'], 0, 5) as $c): ?>
<a href="/sach/<?= esc($c['id']) ?>" title="<?= esc($c['title']) ?>">
<img loading="lazy" src="/thumb/240x320/<?= esc($c['id']) ?>.jpg?v=<?= media_version() ?>" alt="<?= esc($c['title']) ?>"
onerror="this.parentNode.style.display='none'">
</a>
<?php endforeach; ?>
</div>
<a class="grade-head" href="/?lop=<?= $g['grade'] ?>">
<span class="grade-num">Lớp <?= $g['grade'] ?></span>
<span class="grade-count"><?= $g['total'] ?> sách</span>
</a>
<div class="subject-pills">
<?php $i = 0; foreach ($g['subjects'] as $s => $n): if ($i++ >= 8): ?>
<a class="pill more" href="/?lop=<?= $g['grade'] ?>">+ <?= count($g['subjects']) - 8 ?> môn khác</a>
<?php break; endif; ?>
<a class="pill" href="/?lop=<?= $g['grade'] ?>&mon=<?= urlencode($s) ?>"><?= esc($s) ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
<section class="level-section">
<h2 class="level-title">Theo nhà xuất bản / bộ sách</h2>
<div class="pub-grid">
<?php foreach ($publishers as $p => $n): ?>
<a class="pub-card" href="/?nxb=<?= urlencode($p) ?>">
<span class="pub-name"><?= esc($p) ?></span>
<span class="pub-count"><?= $n ?> đầu sách</span>
</a>
<?php endforeach; ?>
</div>
</section>
<?= $this->endSection() ?>
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Xác nhận đọc online <?= esc($book['title']) ?></title>
<meta name="robots" content="noindex, nofollow">
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/app.css">
</head>
<body class="reader-body">
<main class="container">
<div class="gate-card" id="tai-xuong">
<div class="gate-book">
<img src="/thumb/240x320/<?= esc($book['id']) ?>.jpg?v=<?= media_version() ?>" alt="Bìa <?= esc($book['title']) ?>" onerror="this.style.display='none'">
<div>
<h1><?= esc($book['title']) ?></h1>
<p class="gate-sub">
<?= $book['grade'] ? 'Lớp ' . esc($book['grade']) . ' · ' : '' ?><?= esc($book['subject']) ?> · <?= esc($book['publisher']) ?>
<?php if (! empty($book['meta']['pages'])): ?> · <?= (int) $book['meta']['pages'] ?> trang<?php endif; ?>
</p>
<a href="/sach/<?= esc($book['id']) ?>">← Quay lại trang sách</a>
</div>
</div>
<div class="countdown-note">
⏳ Nhập mã captcha và chờ <b id="cd-seconds"><?= $countdown ?></b> giây để tiếp tục đọc online.
Phiên đọc có hiệu lực 2 giờ, bao gồm cả tải xuống PDF.
</div>
<?php if ($error): ?>
<div class="error-box"><?= esc($error) ?></div>
<?php endif; ?>
<form method="post" action="/doc/<?= esc($book['id']) ?>" id="dl-form">
<input type="hidden" name="download" value="1">
<div class="captcha-row">
<img src="/captcha" alt="Mã captcha" id="captcha-img">
<button type="button" class="refresh-captcha" title="Đổi mã khác" onclick="document.getElementById('captcha-img').src='/captcha?'+Date.now()">🔄</button>
<input type="text" name="captcha" required maxlength="5" autocomplete="off" placeholder="Nhập mã">
</div>
<button class="btn btn-primary btn-block" type="submit" id="dl-btn" disabled>
Tiếp tục đọc online
</button>
<p class="gate-legal">⚖️ Tài liệu thuộc bản quyền Nhà xuất bản — chỉ phục vụ mục đích tra cứu, phi thương mại.</p>
</form>
</div>
</main>
<script>
(function () {
var seconds = <?= $countdown ?>;
var btn = document.getElementById('dl-btn');
var s1 = document.getElementById('cd-seconds');
var t = setInterval(function () {
seconds--;
if (s1) s1.textContent = seconds;
if (seconds <= 0) {
clearInterval(t);
btn.disabled = false;
btn.textContent = '✓ Tiếp tục đọc online';
}
}, 1000);
})();
</script>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<div class="hero">
<h1>
<?php if ($filters['q'] !== ''): ?>
Kết quả tìm kiếm "<?= esc($filters['q']) ?>"
<?php elseif ($filters['lop'] !== ''): ?>
Sách lớp <?= esc($filters['lop']) ?>
<?php elseif ($filters['mon'] !== ''): ?>
Môn <?= esc($filters['mon']) ?>
<?php else: ?>
Sách <?= esc($filters['nxb']) ?>
<?php endif; ?>
</h1>
<p><?= number_format($total) ?> đầu sách phù hợp.</p>
</div>
<form class="filters" method="get" action="/">
<div class="grade-pills">
<a href="<?= $filters['q'] !== '' ? '/?q=' . urlencode($filters['q']) : '/' ?>" class="<?= $filters['lop'] === '' ? 'active' : '' ?>">Tất cả</a>
<?php foreach ($grades as $g): ?>
<a class="<?= $filters['lop'] === (string) $g ? 'active' : '' ?>"
href="/?<?= esc(http_build_query(array_merge($_GET, ['trang' => 1, 'lop' => $g]))) ?>">Lớp <?= $g ?></a>
<?php endforeach; ?>
</div>
<label>Môn học
<select name="mon" onchange="this.form.submit()">
<option value="">-- Tất cả --</option>
<?php foreach ($subjects as $s => $n): ?>
<option value="<?= esc($s) ?>" <?= $filters['mon'] === $s ? 'selected' : '' ?>><?= esc($s ?: 'Khác') ?> (<?= $n ?>)</option>
<?php endforeach; ?>
</select>
</label>
<label>Nhà xuất bản
<select name="nxb" onchange="this.form.submit()">
<option value="">-- Tất cả --</option>
<?php foreach ($publishers as $p => $n): ?>
<option value="<?= esc($p) ?>" <?= $filters['nxb'] === $p ? 'selected' : '' ?>><?= esc($p) ?> (<?= $n ?>)</option>
<?php endforeach; ?>
</select>
</label>
<?php if ($filters['q'] !== ''): ?><input type="hidden" name="q" value="<?= esc($filters['q']) ?>"><?php endif; ?>
<button class="btn btn-primary" type="submit">Lọc</button>
<a class="btn btn-outline reset" href="/">Xóa lọc</a>
</form>
<?php foreach ($grouped as $groupLabel => $groupBooks): ?>
<div class="group-header">
<h2><?= esc($groupLabel) ?> <span class="group-count"><?= count($groupBooks) ?> sách</span></h2>
</div>
<div class="book-grid">
<?php foreach ($groupBooks as $b): ?>
<?php $m = $b['meta']; ?>
<a class="book-card" href="/sach/<?= esc($b['id']) ?>">
<div class="book-cover">
<img loading="lazy" src="/thumb/360x480/<?= esc($b['id']) ?>.jpg?v=<?= media_version() ?>" alt="Bìa <?= esc($b['title']) ?>"
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 3 4%22%3E%3Crect width=%223%22 height=%224%22 fill=%22%23e8ecf4%22/%3E%3C/svg%3E'">
<?php if (! empty($m['pages'])): ?><span class="cover-pages"><?= (int) $m['pages'] ?> tr</span><?php endif; ?>
</div>
<div class="book-info">
<h3><?= esc($b['title']) ?></h3>
<?php if ($b['subject']): ?><span class="book-subject"><?= esc($b['subject']) ?></span><?php endif; ?>
<div class="badges">
<?php if ($b['grade']): ?><span class="badge">Lớp <?= esc($b['grade']) ?></span><?php endif; ?>
<?php if ($b['publisher']): ?><span class="badge pub"><?= esc($b['publisher']) ?></span><?php endif; ?>
</div>
<div class="book-meta">
<?php if (! empty($m['sizeMB'])): ?><span>💾 <?= number_format((float) $m['sizeMB'], 1) ?> MB</span><?php endif; ?>
<?php if (! empty($m['authors'])): ?><span class="authors">✍ <?= esc(mb_strimwidth($m['authors'], 0, 46, '…')) ?></span><?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php if (empty($grouped)): ?>
<div class="legal-page"><p>Không tìm thấy sách phù hợp với bộ lọc. <a href="/">Xóa bộ lọc</a>.</p></div>
<?php endif; ?>
<?php if ($totalPage > 1): ?>
<div class="pagination">
<?php
$qs = fn($p) => '/?' . esc(http_build_query(array_merge($_GET, ['trang' => $p])));
?>
<?php if ($page > 1): ?><a href="<?= $qs($page - 1) ?>"> Trước</a><?php else: ?><span class="disabled"> Trước</span><?php endif; ?>
<?php
$start = max(1, $page - 2);
$end = min($totalPage, $start + 4);
$start = max(1, $end - 4);
for ($p = $start; $p <= $end; $p++):
?>
<?php if ($p === $page): ?><span class="active"><?= $p ?></span>
<?php else: ?><a href="<?= $qs($p) ?>"><?= $p ?></a><?php endif; ?>
<?php endfor; ?>
<?php if ($page < $totalPage): ?><a href="<?= $qs($page + 1) ?>">Sau </a><?php else: ?><span class="disabled">Sau </span><?php endif; ?>
</div>
<?php endif; ?>
<?= $this->endSection() ?>
+50
View File
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Đọc online <?= esc($book['title']) ?></title>
<meta name="robots" content="noindex, nofollow">
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/app.css">
</head>
<body class="reader-body">
<header class="reader-header">
<a class="reader-back" href="/sach/<?= esc($book['id']) ?>" title="Về trang sách">←</a>
<div class="reader-title">
<strong><?= esc($book['title']) ?></strong>
<span><?= $book['grade'] ? 'Lớp ' . esc($book['grade']) . ' · ' : '' ?><?= esc($book['subject']) ?> · <?= esc($book['publisher']) ?></span>
</div>
<button class="btn btn-green reader-dl" id="dl-btn" data-id="<?= esc($book['id']) ?>">⬇ Tải PDF</button>
</header>
<main class="reader-main">
<div class="viewer-frame reader-frame" id="pdfViewer" data-src="/media/pdf/<?= esc($book['id']) ?>"></div>
</main>
<script>
(function () {
var el = document.getElementById('pdfViewer');
var iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = '0';
iframe.title = 'Trình đọc PDF';
iframe.src = '/assets/pdfjs/web/viewer.html?file=' + encodeURIComponent(el.dataset.src);
el.appendChild(iframe);
var dl = document.getElementById('dl-btn');
dl.addEventListener('click', function () {
dl.disabled = true; dl.textContent = 'Đang tạo liên kết…';
fetch('/pdf-token/' + dl.dataset.id, { method: 'POST', credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.ok) { window.location.href = d.url; dl.textContent = '⬇ Đang tải xuống…'; }
else { dl.textContent = 'Hết phiên — tải lại trang'; }
})
.catch(function () { dl.textContent = 'Lỗi mạng, thử lại'; dl.disabled = false; });
});
})();
</script>
</body>
</html>
+60
View File
@@ -0,0 +1,60 @@
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<?php $meta = $book['meta']; ?>
<div class="book-detail">
<div>
<div class="detail-cover">
<img src="/media/cover/<?= esc($book['id']) ?>?v=<?= media_version() ?>" alt="Bìa sách <?= esc($book['title']) ?>"
onerror="this.style.display='none'">
</div>
<div class="actions" style="flex-direction:column">
<a class="btn btn-primary" href="/doc/<?= esc($book['id']) ?>">📖 Đọc online</a>
<a class="btn btn-green" href="/doc/<?= esc($book['id']) ?>#tai-xuong">⬇ Tải xuống PDF</a>
</div>
</div>
<div class="detail-info">
<h1><?= esc($book['title']) ?></h1>
<table class="meta-table">
<?php if ($book['grade']): ?>
<tr><th>Lớp</th><td>Lớp <?= esc($book['grade']) ?></td></tr>
<?php endif; ?>
<?php if ($book['subject']): ?>
<tr><th>Môn học</th><td><?= esc($book['subject']) ?></td></tr>
<?php endif; ?>
<tr><th>Nhà xuất bản / Bộ sách</th><td><?= esc($book['publisher']) ?><?= ! empty($meta['publisherFull']) ? ' — ' . esc($meta['publisherFull']) : '' ?></td></tr>
<?php if (! empty($meta['authors'])): ?>
<tr><th>Tác giả / Chủ biên</th><td><?= esc($meta['authors']) ?></td></tr>
<?php endif; ?>
<?php if (! empty($meta['pages'])): ?>
<tr><th>Số trang</th><td><?= (int) $meta['pages'] ?> trang</td></tr>
<?php endif; ?>
<?php if (! empty($meta['sizeMB'])): ?>
<tr><th>Dung lượng</th><td><?= number_format((float) $meta['sizeMB'], 2) ?> MB</td></tr>
<?php endif; ?>
<?php if (! empty($meta['isbn'])): ?>
<tr><th>ISBN</th><td><?= esc($meta['isbn']) ?></td></tr>
<?php endif; ?>
<?php if (! empty($meta['price'])): ?>
<tr><th>Giá bìa</th><td><?= esc($meta['price']) ?></td></tr>
<?php endif; ?>
<?php if (! empty($meta['pdfVersion'])): ?>
<tr><th>Phiên bản PDF</th><td>PDF <?= esc($meta['pdfVersion']) ?></td></tr>
<?php endif; ?>
<?php if (! empty($meta['creationDate'])): ?>
<tr><th>Ngày tạo tài liệu</th><td><?= esc($meta['creationDate']) ?></td></tr>
<?php endif; ?>
<tr><th>Lượt tải</th><td><?= number_format((int) $downloads) ?></td></tr>
</table>
<div class="countdown-note">
⚖️ <strong>Lưu ý bản quyền:</strong> Tài liệu này thuộc bản quyền của Nhà xuất bản và Bộ Giáo dục &amp; Đào tạo.
Nội dung chỉ được hiển thị phục vụ mục đích tra cứu, tham khảo. Vui lòng mua sách giấy chính hãng để hỗ trợ tác giả và nhà xuất bản.
Báo cáo vi phạm <a href="/lien-he">tại đây</a>.
</div>
</div>
</div>
<?= $this->endSection() ?>