diff --git a/app.py b/app.py
index 25f7c6a..b5e4b1f 100644
--- a/app.py
+++ b/app.py
@@ -3,10 +3,12 @@ from flask import Flask
from config import Config
from routes.transcriptionRoute import transcription_bp
from routes.scraperZLibraryRoute import scraperZLibraryRoute
+from routes.headerRoute import header_bp
def create_app():
"""Application factory function"""
- app = Flask(__name__, template_folder='views')
+ # static_folder=None → tắt hoàn toàn thư mục /static mặc định của Flask
+ app = Flask(__name__, template_folder='views', static_folder=None)
# Load configuration
app.config.from_object(Config)
@@ -18,6 +20,7 @@ def create_app():
Config.init_app()
# Register blueprints
+ app.register_blueprint(header_bp)
app.register_blueprint(transcription_bp)
app.register_blueprint(scraperZLibraryRoute, url_prefix='/scraperZLibrary')
diff --git a/routes/headerRoute.py b/routes/headerRoute.py
new file mode 100644
index 0000000..228db15
--- /dev/null
+++ b/routes/headerRoute.py
@@ -0,0 +1,11 @@
+# routes/headerRoute.py
+from flask import Blueprint
+
+# Blueprint để serve headerLoader.js từ views/header/
+# → URL: /assets/header/headerLoader.js
+header_bp = Blueprint(
+ 'header',
+ __name__,
+ static_folder='../views/header',
+ static_url_path='/assets/header',
+)
diff --git a/routes/scraperZLibraryRoute.py b/routes/scraperZLibraryRoute.py
index da86e65..ebf0598 100644
--- a/routes/scraperZLibraryRoute.py
+++ b/routes/scraperZLibraryRoute.py
@@ -2,8 +2,15 @@
from flask import Blueprint
from controllers.scraperZLibraryController import ScraperController
-# Create blueprint
-scraperZLibraryRoute = Blueprint('scraperZLibrary', __name__)
+# Blueprint với static_folder trỏ vào views/scraperZLibraryPages
+# → Flask serve CSS/JS qua URL /scraperZLibrary/assets/scraper/...
+# Lưu ý: static_url_path chỉ cần phần sau /scraperZLibrary/ (url_prefix đã được thêm khi register)
+scraperZLibraryRoute = Blueprint(
+ 'scraperZLibrary',
+ __name__,
+ static_folder='../views/scraperZLibraryPages',
+ static_url_path='/assets/scraper',
+)
# Define routes
scraperZLibraryRoute.route('/', methods=['GET'])(ScraperController.index)
diff --git a/routes/transcriptionRoute.py b/routes/transcriptionRoute.py
index 88c82e9..3fdb84a 100644
--- a/routes/transcriptionRoute.py
+++ b/routes/transcriptionRoute.py
@@ -2,8 +2,14 @@
from flask import Blueprint
from controllers.transcriptionController import TranscriptionController
-# Create blueprint
-transcription_bp = Blueprint('transcription', __name__)
+# Blueprint với static_folder trỏ vào views/transcriptionPages
+# → Flask serve CSS/JS qua URL /assets/transcription/...
+transcription_bp = Blueprint(
+ 'transcription',
+ __name__,
+ static_folder='../views/transcriptionPages',
+ static_url_path='/assets/transcription',
+)
# Define routes
transcription_bp.route('/', methods=['GET'])(TranscriptionController.index)
diff --git a/static/header/header.html b/static/header/header.html
deleted file mode 100644
index 608fd00..0000000
--- a/static/header/header.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
diff --git a/static/header/headerLoader.js b/static/header/headerLoader.js
deleted file mode 100644
index 8cecb41..0000000
--- a/static/header/headerLoader.js
+++ /dev/null
@@ -1,187 +0,0 @@
-// static/header/headerLoader.js
-(function () {
- "use strict";
-
- // Configuration
- const CONFIG = {
- headerPath: "/static/header/header.html",
- insertPosition: "afterbegin",
- containerSelector: "body",
- activeClass: "active",
- version: "1.0",
- };
-
- // Styles
- const styles = `
-
- `;
-
- // Load header function
- async function loadHeader() {
- try {
- // Fetch header HTML
- const response = await fetch(`${CONFIG.headerPath}?v=${CONFIG.version}`);
-
- if (!response.ok) {
- throw new Error(`Failed to load header: ${response.status}`);
- }
-
- const headerHTML = await response.text();
-
- // Inject styles if not already present
- if (!document.getElementById("header-component-styles")) {
- document.head.insertAdjacentHTML("beforeend", styles);
- }
-
- // Insert header
- const container = document.querySelector(CONFIG.containerSelector);
- container.insertAdjacentHTML(CONFIG.insertPosition, headerHTML);
-
- // Highlight active link
- highlightActiveLink();
-
- console.log("✅ Header component loaded v" + CONFIG.version);
- } catch (error) {
- console.error("❌ Error loading header:", error);
- createFallbackHeader();
- }
- }
-
- // Highlight current page link
- function highlightActiveLink() {
- const currentPath = window.location.pathname;
- const links = document.querySelectorAll(".nav-link");
-
- links.forEach((link) => {
- const href = link.getAttribute("href");
-
- // Check if current path matches
- if (
- currentPath === href ||
- (href !== "/" && currentPath.startsWith(href))
- ) {
- link.classList.add(CONFIG.activeClass);
- }
-
- // Special case for root
- if (currentPath === "/" && href === "/transcription") {
- link.classList.add(CONFIG.activeClass);
- }
- });
- }
-
- // Fallback header in case of error
- function createFallbackHeader() {
- const fallback = document.createElement("div");
- fallback.style.cssText = `
- padding: 15px 20px;
- background: #fff;
- border-bottom: 1px solid #e0e0e0;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 1000;
- `;
- fallback.innerHTML = `
-
- `;
- document.body.insertAdjacentElement("afterbegin", fallback);
- document.body.style.paddingTop = "50px";
- }
-
- // Initialize when DOM is ready
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", loadHeader);
- } else {
- loadHeader();
- }
-})();
diff --git a/views/header/headerLoader.js b/views/header/headerLoader.js
new file mode 100644
index 0000000..38f961b
--- /dev/null
+++ b/views/header/headerLoader.js
@@ -0,0 +1,234 @@
+// views/header/headerLoader.js
+(function () {
+ "use strict";
+
+ // Configuration
+ const CONFIG = {
+ insertPosition: "afterbegin",
+ containerSelector: "body",
+ activeClass: "active",
+ version: "1.1",
+ };
+
+ // Header HTML inlined — không cần fetch file nữa
+ const headerHTML = `
+`;
+
+ const styles = `
+
+ `;
+
+ // Inject header directly (no fetch needed)
+ function loadHeader() {
+ // Inject styles if not already present
+ if (!document.getElementById("header-component-styles")) {
+ document.head.insertAdjacentHTML("beforeend", styles);
+ }
+
+ // Insert header HTML inline
+ const container = document.querySelector(CONFIG.containerSelector);
+ container.insertAdjacentHTML(CONFIG.insertPosition, headerHTML);
+
+ // Highlight active link
+ highlightActiveLink();
+
+ console.log("✅ Header component loaded v" + CONFIG.version);
+ }
+
+ // Highlight current page link
+ function highlightActiveLink() {
+ const currentPath = window.location.pathname;
+ const links = document.querySelectorAll(".nav-link");
+
+ links.forEach((link) => {
+ const href = link.getAttribute("href");
+
+ // Check if current path matches
+ if (
+ currentPath === href ||
+ (href !== "/" && currentPath.startsWith(href))
+ ) {
+ link.classList.add(CONFIG.activeClass);
+ }
+
+ // Special case for root
+ if (currentPath === "/" && href === "/transcription") {
+ link.classList.add(CONFIG.activeClass);
+ }
+ });
+ }
+
+ // Initialize when DOM is ready
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", loadHeader);
+ } else {
+ loadHeader();
+ }
+})();
diff --git a/views/scraperZLibraryPages/scraperZLibraryMain.css b/views/scraperZLibraryPages/scraperZLibraryMain.css
new file mode 100644
index 0000000..0726b59
--- /dev/null
+++ b/views/scraperZLibraryPages/scraperZLibraryMain.css
@@ -0,0 +1,436 @@
+/* ═══════════════════════════════════════════════════════════
+ scraperZLibraryMain.css · Mainframe Design System v3
+ ═══════════════════════════════════════════════════════════ */
+
+:root {
+ --font-heading: 'HelveticaNowDisplay-Medium','Helvetica Neue',Arial,sans-serif;
+ --font-body: 'HelveticaNowDisplayW01-Rg','Helvetica Neue',Arial,sans-serif;
+ --ink: #0a0a0a;
+ --ink-70: rgba(10,10,10,0.70);
+ --ink-45: rgba(10,10,10,0.45);
+ --ink-20: rgba(10,10,10,0.20);
+ --ink-10: rgba(10,10,10,0.10);
+ --ink-06: rgba(10,10,10,0.06);
+ --ink-03: rgba(10,10,10,0.03);
+ --surface: #f4f3ef;
+ --surface-2: #eeecea;
+ --card: #ffffff;
+ --radius-pill: 9999px;
+ --radius-xl: 24px;
+ --radius-lg: 18px;
+ --radius-md: 12px;
+ --shadow-xs: 0 1px 2px rgba(0,0,0,0.06);
+ --shadow-sm: 0 2px 10px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
+ --shadow-md: 0 6px 28px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.05);
+ --shadow-inset: inset 0 1px 0 rgba(255,255,255,0.8);
+}
+
+@keyframes fade-up {
+ from { opacity:0; transform:translateY(14px); }
+ to { opacity:1; transform:none; }
+}
+@keyframes spin { to { transform:rotate(360deg); } }
+
+*,*::before,*::after { margin:0; padding:0; box-sizing:border-box; }
+
+html {
+ font-family: var(--font-body);
+ -webkit-font-smoothing: antialiased;
+ background: var(--surface);
+ color: var(--ink);
+}
+
+body {
+ min-height: 100vh;
+ background: var(--surface);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.028'/%3E%3C/svg%3E");
+ padding: 0 28px 100px;
+ font-family: var(--font-body);
+}
+
+/* ─── Layout ─────────────────────────────────────────────── */
+.container {
+ max-width: 1260px;
+ margin: 0 auto;
+ animation: fade-up .65s cubic-bezier(.22,1,.36,1) backwards;
+ animation-delay: .04s;
+}
+
+/* ─── Page Header ────────────────────────────────────────── */
+.header {
+ padding: 42px 0 28px;
+ border-bottom: 1px solid var(--ink-10);
+ margin-bottom: 0;
+}
+
+/* Eyebrow */
+.header::before {
+ content: '✦ Z-Library Search';
+ display: inline-block;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: .1em;
+ text-transform: uppercase;
+ color: var(--ink-45);
+ margin-bottom: 14px;
+ padding: 6px 14px;
+ background: var(--card);
+ border: 1px solid var(--ink-10);
+ border-radius: var(--radius-pill);
+ box-shadow: var(--shadow-xs);
+}
+
+.header h1 {
+ font-family: var(--font-heading);
+ font-size: clamp(30px, 5.5vw, 50px);
+ font-weight: 500;
+ letter-spacing: -0.055em;
+ line-height: 1.0;
+ color: var(--ink);
+ margin-bottom: 8px;
+}
+
+.header p {
+ font-size: 14px;
+ color: var(--ink-45);
+ line-height: 1.5;
+}
+
+/* ─── Search Section ─────────────────────────────────────── */
+.search-section {
+ padding: 26px 0;
+ border-bottom: 1px solid var(--ink-06);
+}
+
+.search-form {
+ display: flex;
+ gap: 10px;
+ align-items: flex-end;
+ flex-wrap: wrap;
+}
+
+.form-group { flex:1; min-width:200px; }
+
+.form-group label {
+ display: block;
+ margin-bottom: 7px;
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .1em;
+ color: var(--ink-45);
+}
+
+.form-group input {
+ width: 100%;
+ padding: 11px 18px;
+ border: 1px solid var(--ink-10);
+ border-radius: var(--radius-pill);
+ font-size: 14px;
+ font-family: var(--font-body);
+ background: var(--card);
+ color: var(--ink);
+ box-shadow: var(--shadow-xs), var(--shadow-inset);
+ transition: border-color .2s, box-shadow .2s;
+ -webkit-appearance: none;
+}
+.form-group input:focus {
+ outline: none;
+ border-color: var(--ink-45);
+ box-shadow: 0 0 0 3px rgba(10,10,10,.07), var(--shadow-inset);
+}
+.form-group input::placeholder { color: var(--ink-20); }
+.form-group small {
+ display: block;
+ margin-top: 6px;
+ font-size: 11px;
+ color: var(--ink-45);
+ padding-left: 10px;
+}
+
+/* ─── Checkbox pills ─────────────────────────────────────── */
+.checkbox-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 16px;
+}
+
+.checkbox-group label {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ font-size: 13px;
+ letter-spacing: -0.01em;
+ color: var(--ink-70);
+ background: var(--card);
+ border: 1px solid var(--ink-10);
+ border-radius: var(--radius-pill);
+ padding: 6px 16px;
+ cursor: pointer;
+ box-shadow: var(--shadow-xs), var(--shadow-inset);
+ user-select: none;
+ transition: background .18s, color .18s, border-color .18s, box-shadow .18s;
+}
+.checkbox-group label:hover {
+ background: var(--ink);
+ color: #fff;
+ border-color: var(--ink);
+ box-shadow: var(--shadow-sm);
+}
+.checkbox-group input[type="checkbox"] {
+ accent-color: var(--ink);
+ width: 13px; height: 13px; cursor: pointer;
+}
+
+/* ─── Buttons ────────────────────────────────────────────── */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 9px 22px;
+ border-radius: var(--radius-pill);
+ font-size: 13px;
+ font-family: var(--font-body);
+ letter-spacing: -0.01em;
+ cursor: pointer;
+ border: 1px solid var(--ink-10);
+ text-decoration: none;
+ white-space: nowrap;
+ background: var(--card);
+ color: var(--ink);
+ box-shadow: var(--shadow-xs), var(--shadow-inset);
+ margin: 0 5px 5px 0;
+ line-height: 1;
+ position: relative;
+ overflow: hidden;
+ transition: background .2s, color .2s, border-color .2s, box-shadow .2s,
+ transform .18s cubic-bezier(.22,1,.36,1);
+}
+.btn:hover:not(:disabled) {
+ background: var(--ink);
+ color: #fff;
+ border-color: var(--ink);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-2px);
+}
+.btn:active:not(:disabled) { transform:scale(.97) translateY(0); box-shadow:var(--shadow-xs); }
+.btn:disabled { opacity:.35; cursor:not-allowed; }
+
+.btn-primary { background:var(--ink); color:#fff; border-color:var(--ink); box-shadow:var(--shadow-sm); }
+.btn-primary:hover:not(:disabled) { background:#1a1a1a; }
+
+.btn-download { background:var(--ink); color:#fff; border-color:var(--ink); box-shadow:var(--shadow-sm); }
+.btn-download:hover:not(:disabled) { background:#1a1a1a; }
+
+/* ─── Results Section ────────────────────────────────────── */
+.results-section { padding: 26px 0; }
+
+/* ─── Stats Bar — horizontal card row ───────────────────── */
+.stats-bar {
+ display: flex;
+ background: var(--card);
+ border: 1px solid var(--ink-10);
+ border-radius: var(--radius-xl);
+ margin-bottom: 26px;
+ overflow: hidden;
+ box-shadow: var(--shadow-sm), var(--shadow-inset);
+ flex-wrap: wrap;
+}
+.stat-item {
+ flex: 1;
+ min-width: 120px;
+ padding: 22px 26px;
+ border-right: 1px solid var(--ink-06);
+ position: relative;
+}
+.stat-item:last-child { border-right: none; }
+.stat-item::before {
+ content: '';
+ position: absolute;
+ top: 0; left: 26px; right: 26px;
+ height: 1.5px;
+ background: linear-gradient(90deg, transparent 0%, var(--ink-10) 50%, transparent 100%);
+}
+.stat-label {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .1em;
+ color: var(--ink-45);
+ margin-bottom: 6px;
+}
+.stat-value {
+ font-family: var(--font-heading);
+ font-size: clamp(24px, 3.5vw, 34px);
+ font-weight: 500;
+ letter-spacing: -0.05em;
+ color: var(--ink);
+ line-height: 1.05;
+}
+
+/* ─── Books Grid ─────────────────────────────────────────── */
+.books-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
+ gap: 1px;
+ background: var(--ink-10);
+ border: 1px solid var(--ink-10);
+ border-radius: var(--radius-xl);
+ overflow: hidden;
+ margin-top: 18px;
+ box-shadow: var(--shadow-sm);
+}
+
+.book-card {
+ background: var(--card);
+ padding: 22px 20px 18px;
+ position: relative;
+ transition: background .18s;
+}
+.book-card:hover { background: #fdfcf9; }
+.book-card.selected {
+ background: rgba(10,10,10,0.025);
+ box-shadow: inset 3px 0 0 var(--ink);
+}
+
+.book-checkbox {
+ position: absolute;
+ top: 18px; right: 18px;
+ width: 15px; height: 15px;
+ accent-color: var(--ink);
+ cursor: pointer;
+}
+
+.book-title {
+ font-size: 14px;
+ font-weight: 500;
+ letter-spacing: -0.02em;
+ color: var(--ink);
+ margin-bottom: 4px;
+ line-height: 1.4;
+ padding-right: 26px;
+}
+.book-authors {
+ font-size: 12px;
+ color: var(--ink-45);
+ margin-bottom: 14px;
+}
+
+.book-details {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px 10px;
+ margin-bottom: 14px;
+ padding-bottom: 14px;
+ border-bottom: 1px solid var(--ink-06);
+}
+.detail-item { font-size: 11px; }
+.detail-label {
+ color: var(--ink-45);
+ text-transform: uppercase;
+ font-size: 9.5px;
+ letter-spacing: .07em;
+ font-weight: 700;
+ margin-bottom: 1px;
+}
+.detail-value { color: var(--ink-70); }
+
+.book-link {
+ display: inline-block;
+ font-size: 12px;
+ color: var(--ink);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-color: var(--ink-20);
+ transition: text-decoration-color .18s;
+}
+.book-link:hover { text-decoration-color: var(--ink); }
+
+/* ─── Loading ─────────────────────────────────────────────── */
+.loading { text-align:center; padding:80px 20px; display:none; }
+.spinner {
+ width:26px; height:26px;
+ border:1.5px solid var(--ink-10);
+ border-top-color: var(--ink);
+ border-radius:50%;
+ animation:spin .7s linear infinite;
+ margin:0 auto 16px;
+}
+.loading p { font-size:13px; color:var(--ink-45); }
+
+/* ─── Misc ───────────────────────────────────────────────── */
+.progress-container { margin-top:12px; }
+.progress-text { font-size:11px; color:var(--ink-45); margin-top:5px; }
+
+.error-message {
+ background:var(--card);
+ border:1px solid var(--ink-10);
+ color:var(--ink-70);
+ padding:12px 18px;
+ border-radius:var(--radius-md);
+ margin:14px 0;
+ font-size:13px;
+ display:none;
+ box-shadow:var(--shadow-xs);
+}
+
+.empty-state { text-align:center; padding:80px 24px; color:var(--ink-45); }
+.empty-state h3 {
+ font-family:var(--font-heading);
+ font-size:20px;
+ font-weight:500;
+ letter-spacing:-0.04em;
+ color:var(--ink);
+ margin-bottom:8px;
+}
+.empty-state p { font-size:14px; }
+
+.download-section { display:flex; gap:6px; flex-wrap:wrap; margin:18px 0; }
+
+.selection-bar {
+ display:flex;
+ align-items:center;
+ justify-content:space-between;
+ padding:12px 0;
+ border-bottom:1px solid var(--ink-06);
+ margin-bottom:14px;
+ flex-wrap:wrap;
+ gap:10px;
+}
+.selection-controls { display:flex; align-items:center; gap:10px; }
+.selection-controls label {
+ display:inline-flex;
+ align-items:center;
+ gap:7px;
+ font-size:13px;
+ color:var(--ink);
+ cursor:pointer;
+}
+.selection-controls label input { accent-color:var(--ink); }
+.selection-info { font-size:12px; color:var(--ink-45); }
+
+.stats-detail { margin-top:30px; padding-top:22px; border-top:1px solid var(--ink-10); }
+.stats-detail h3 {
+ font-size:10px;
+ font-weight:700;
+ letter-spacing:.1em;
+ text-transform:uppercase;
+ color:var(--ink-45);
+ margin-bottom:16px;
+}
+
+hr { border:none; border-top:1px solid var(--ink-06); margin:14px 0; }
+
+.load-more-container { margin-top:28px; text-align:center; }
+.load-more-btn { width:100%; padding:13px; font-size:13px; }
+
+#resultCountHeader {
+ font-size:10px;
+ font-weight:700;
+ letter-spacing:.1em;
+ text-transform:uppercase;
+ color:var(--ink-45);
+ margin:20px 0 14px;
+}
diff --git a/views/scraperZLibraryPages/scraperZLibraryMain.html b/views/scraperZLibraryPages/scraperZLibraryMain.html
index 21a3c29..0438d9b 100644
--- a/views/scraperZLibraryPages/scraperZLibraryMain.html
+++ b/views/scraperZLibraryPages/scraperZLibraryMain.html
@@ -5,406 +5,9 @@
Tìm Kiếm Sách Z-Library
-
+
+
+
@@ -470,761 +73,7 @@
-
-
+
+