From d9a8b246f2971a25d9c949c67f305f502d02b1fe Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 25 Mar 2026 07:49:33 +0000 Subject: [PATCH] Autosave: 20260325-074933 --- admin.php | 390 ++++++++++ api/translate.php | 46 ++ assets/css/custom.css | 686 ++++++++---------- assets/js/main.js | 48 +- db/migrations/001_library_documents.sql | 23 + db/migrations/002_add_library_metadata.sql | 8 + db/migrations/003_normalize_categories.sql | 18 + document.php | 162 +++++ includes/layout.php | 103 +++ includes/library.php | 364 ++++++++++ index.php | 336 +++++---- ...25065048-demo-bilingual-notes-d11a1716.txt | 5 + uploads/library/demo-bilingual-notes.txt | 5 + uploads/library/demo-library-guide.pdf | 45 ++ 14 files changed, 1688 insertions(+), 551 deletions(-) create mode 100644 admin.php create mode 100644 api/translate.php create mode 100644 db/migrations/001_library_documents.sql create mode 100644 db/migrations/002_add_library_metadata.sql create mode 100644 db/migrations/003_normalize_categories.sql create mode 100644 document.php create mode 100644 includes/layout.php create mode 100644 includes/library.php create mode 100644 uploads/library/20260325065048-demo-bilingual-notes-d11a1716.txt create mode 100644 uploads/library/demo-bilingual-notes.txt create mode 100644 uploads/library/demo-library-guide.pdf diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..a8df133 --- /dev/null +++ b/admin.php @@ -0,0 +1,390 @@ +getMessage(); + } +} + +$documents = library_fetch_documents(false, []); +$metrics = library_catalog_metrics(); +$categories = library_get_categories(); +// Fetch all subcategories to pass to JS for filtering +$allSubcategories = library_get_subcategories(null); + +$flashes = library_get_flashes(); +?> + + + + + + Admin Studio · Nabd Library + + + + + +
+ +
+

Catalog Manager

+

Upload manuscripts and manage permissions.

+ + + + + + +
+ + +
+ +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ +

Recent Documents

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDTitleCategoryVisibilityActions
No documents found.
+
+ +
+ + + > + + > + + + + + + + View +
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/api/translate.php b/api/translate.php new file mode 100644 index 0000000..0588300 --- /dev/null +++ b/api/translate.php @@ -0,0 +1,46 @@ + 'Method not allowed']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true); +$text = trim($input['text'] ?? ''); +$targetLang = trim($input['target_lang'] ?? ''); + +if (!$text || !$targetLang) { + http_response_code(400); + echo json_encode(['error' => 'Missing text or target_lang']); + exit; +} + +$prompt = "Translate the following text to {$targetLang}. Provide ONLY the translation, without any additional text, quotes, or explanations."; + +$response = LocalAIApi::createResponse([ + 'model' => 'gemini-2.0-flash-001', // Using a fast model if available, or default + 'input' => [ + ['role' => 'system', 'content' => 'You are a helpful translator. Translate the user input accurately. Output only the translated text.'], + ['role' => 'user', 'content' => $text], + ], +]); + +if (empty($response['success'])) { + http_response_code(500); + echo json_encode(['error' => 'AI translation failed', 'details' => $response['error'] ?? 'Unknown error']); + exit; +} + +$translatedText = LocalAIApi::extractText($response); + +// Clean up any accidental quotes if the model adds them despite instructions +$translatedText = trim($translatedText, " \t\n\r\0\x0B\"'"); + +echo json_encode(['translation' => $translatedText]); diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..2e1905e 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,349 @@ +:root { + --bg: #f5f6f7; + --surface: #ffffff; + --surface-muted: #f0f2f4; + --border: #d8dde3; + --border-strong: #bcc5cf; + --text: #111827; + --text-secondary: #5b6573; + --accent: #1f2937; + --accent-soft: #eef1f4; + --success: #0f766e; + --warning: #92400e; + --radius-sm: 0.5rem; + --radius-md: 0.75rem; + --radius-lg: 1rem; + --shadow-sm: 0 8px 24px rgba(15, 23, 42, 0.04); + --shadow-md: 0 18px 40px rgba(15, 23, 42, 0.06); +} + +html { + scroll-behavior: smooth; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; + background: var(--bg); + color: var(--text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; min-height: 100vh; } -.main-wrapper { - display: flex; +::selection { + background: #dbe2ea; +} + +.app-shell { + min-height: 100vh; +} + +.navbar { + backdrop-filter: blur(8px); +} + +.brand-mark { + width: 2.25rem; + height: 2.25rem; + border-radius: 0.75rem; + display: inline-flex; align-items: center; justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); + background: var(--accent); + color: #fff; font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; + letter-spacing: 0.04em; font-size: 0.9rem; } -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; +.brand-title { + font-weight: 700; + color: var(--text); + line-height: 1.1; } -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +.nav-link { + color: var(--text-secondary); + font-weight: 500; + border-radius: var(--radius-sm); + padding-inline: 0.85rem !important; } -.header-container { - display: flex; - justify-content: space-between; +.nav-link.active, +.nav-link:hover, +.nav-link:focus { + color: var(--text); + background: var(--accent-soft); +} + +.hero-surface, +.panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.hero-surface { + padding: clamp(1.5rem, 2vw, 2rem); +} + +.panel { + padding: 1.35rem; +} + +.eyebrow, +.section-kicker { + display: inline-flex; align-items: center; + gap: 0.35rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.72rem; + font-weight: 700; + margin-bottom: 0.9rem; } -.header-links { +.display-6, +.h3, +.h4, +.h5 { + letter-spacing: -0.03em; +} + +.lead { + font-size: 1.04rem; +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.9rem; +} + +.metric-grid-compact { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.metric-card, +.recent-card { + background: var(--surface-muted); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 1rem; +} + +.metric-card { display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.metric-value { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.04em; +} + +.metric-label { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.compact-list, +.compact-list-numbered { + color: var(--text-secondary); + display: grid; + gap: 0.65rem; + padding-left: 1.2rem; +} + +.compact-list { + padding-left: 1rem; +} + +.compact-list li::marker, +.compact-list-numbered li::marker { + color: var(--text); +} + +.link-arrow, +.back-link { + color: var(--text); + font-weight: 600; + text-decoration: none; +} + +.link-arrow:hover, +.back-link:hover { + color: #000; +} + +.form-control, +.form-select { + border-color: var(--border-strong); + padding: 0.72rem 0.85rem; + border-radius: var(--radius-sm); + background: #fff; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.nav-link:focus, +.btn-close:focus { + box-shadow: 0 0 0 0.2rem rgba(31, 41, 55, 0.12); + border-color: #9aa5b1; +} + +.form-text, +.text-secondary { + color: var(--text-secondary) !important; +} + +.btn { + border-radius: 0.65rem; + padding: 0.7rem 1rem; + font-weight: 600; +} + +.btn-dark { + background: var(--accent); + border-color: var(--accent); +} + +.btn-dark:hover, +.btn-dark:focus { + background: #111827; + border-color: #111827; +} + +.btn-outline-secondary { + color: var(--text); + border-color: var(--border-strong); +} + +.btn-outline-secondary:hover, +.btn-outline-secondary:focus { + background: var(--accent-soft); + color: var(--text); + border-color: var(--border-strong); +} + +.badge { + border-radius: 999px; + font-weight: 600; + letter-spacing: 0.01em; + padding: 0.55em 0.75em; +} + +.text-bg-light { + background: var(--accent-soft) !important; + color: var(--text) !important; + border: 1px solid var(--border); +} + +.empty-panel { + background: var(--surface); +} + +.empty-icon { + width: 3rem; + height: 3rem; + border-radius: 1rem; + background: var(--surface-muted); + border: 1px solid var(--border); + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 1.1rem; +} + +.table > :not(caption) > * > * { + padding-block: 0.95rem; + border-bottom-color: var(--border); +} + +.table thead th { + color: var(--text-secondary); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.reader-panel { + padding-bottom: 1rem; +} + +.reader-frame-wrap { + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + background: #d1d5db; +} + +.reader-frame { + width: 100%; + min-height: 70vh; + border: 0; + background: #fff; +} + +.reader-lock, +.summary-box { + background: var(--surface-muted); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 1rem; +} + +.summary-box { + white-space: pre-line; + line-height: 1.7; +} + +.summary-box-muted { + color: var(--text-secondary); +} + +.description-stack { + display: grid; gap: 1rem; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); +.recent-card { + display: block; + height: 100%; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; } -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; +.recent-card:hover, +.recent-card:focus { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: var(--border-strong); } -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; +.toast { + min-width: 280px; + border-radius: 0.85rem; + overflow: hidden; } -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; +.toast-stack { + z-index: 1090; } -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; +footer a { + color: var(--text); } -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} +@media (max-width: 991.98px) { + .metric-grid { + grid-template-columns: 1fr; + } -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); + .reader-frame { + min-height: 60vh; + } } - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..ab57e25 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,17 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + document.querySelectorAll('.toast').forEach((toastNode) => { + const toast = new bootstrap.Toast(toastNode); + toast.show(); + }); - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + document.querySelectorAll('[data-file-input]').forEach((input) => { + input.addEventListener('change', () => { + const help = input.closest('.col-12')?.querySelector('.form-text'); + if (!help) { + return; + } + const fileName = input.files && input.files[0] ? input.files[0].name : 'Supported: PDF, TXT, DOC, DOCX, PPT, PPTX. Max 12 MB.'; + help.textContent = fileName; + }); }); }); diff --git a/db/migrations/001_library_documents.sql b/db/migrations/001_library_documents.sql new file mode 100644 index 0000000..766e882 --- /dev/null +++ b/db/migrations/001_library_documents.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS library_documents ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + title_en VARCHAR(255) DEFAULT NULL, + title_ar VARCHAR(255) DEFAULT NULL, + author_name VARCHAR(255) DEFAULT NULL, + document_language ENUM('en', 'ar', 'bilingual') NOT NULL DEFAULT 'bilingual', + visibility ENUM('public', 'private') NOT NULL DEFAULT 'public', + document_type VARCHAR(50) NOT NULL DEFAULT 'pdf', + file_name VARCHAR(255) DEFAULT NULL, + file_path VARCHAR(255) DEFAULT NULL, + file_size_kb INT UNSIGNED DEFAULT NULL, + description_en TEXT DEFAULT NULL, + description_ar TEXT DEFAULT NULL, + summary_text TEXT DEFAULT NULL, + tags VARCHAR(255) DEFAULT NULL, + is_featured TINYINT(1) NOT NULL DEFAULT 0, + view_count INT UNSIGNED NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_library_visibility_created (visibility, created_at), + KEY idx_library_language_visibility (document_language, visibility) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/db/migrations/002_add_library_metadata.sql b/db/migrations/002_add_library_metadata.sql new file mode 100644 index 0000000..27f4156 --- /dev/null +++ b/db/migrations/002_add_library_metadata.sql @@ -0,0 +1,8 @@ +ALTER TABLE library_documents +ADD COLUMN category VARCHAR(100) DEFAULT NULL, +ADD COLUMN category_ar VARCHAR(100) DEFAULT NULL, +ADD COLUMN sub_category VARCHAR(100) DEFAULT NULL, +ADD COLUMN sub_category_ar VARCHAR(100) DEFAULT NULL, +ADD COLUMN allow_download TINYINT(1) NOT NULL DEFAULT 0, +ADD COLUMN allow_print TINYINT(1) NOT NULL DEFAULT 0, +ADD COLUMN allow_copy TINYINT(1) NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/db/migrations/003_normalize_categories.sql b/db/migrations/003_normalize_categories.sql new file mode 100644 index 0000000..bc87971 --- /dev/null +++ b/db/migrations/003_normalize_categories.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS library_categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name_en VARCHAR(255) NOT NULL, + name_ar VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS library_subcategories ( + id INT AUTO_INCREMENT PRIMARY KEY, + category_id INT NOT NULL, + name_en VARCHAR(255) NOT NULL, + name_ar VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES library_categories(id) ON DELETE CASCADE +); + +ALTER TABLE library_documents ADD COLUMN IF NOT EXISTS category_id INT NULL; +ALTER TABLE library_documents ADD COLUMN IF NOT EXISTS subcategory_id INT NULL; diff --git a/document.php b/document.php new file mode 100644 index 0000000..331479e --- /dev/null +++ b/document.php @@ -0,0 +1,162 @@ + 0 ? library_fetch_document($documentId, $publicOnly) : null; + +if (!$document) { + http_response_code(404); + library_render_header('Document not found', 'The requested library document could not be found.', $context === 'admin' ? 'admin' : 'catalog'); + ?> +
+
?
+

Document not found

+

This item is unavailable or private.

+ Go back +
+ +
+ ← Back to +
+ +
+
+
+
+
+ +

+ + +
+ +
+
+ + + +
+
+ +
+
Author
+
Views
+
File
+
+
+ +
+
+
+
Online reader
+

Read in the browser

+
+ + Open file + +
+ + +
+

Private item

+

This title is marked as login-required by the admin, so it stays hidden from the public reading experience.

+
+ +
+ +
+ +
+

Document stored

+

This file type is stored successfully, but inline reading is optimized for PDF in this first slice.

+ Download / open file +
+ +
+

No file attached

+

Upload a file from the Admin Studio to enable reading.

+
+ +
+
+ +
+
+
+
+
AI summary
+

Bilingual quick summary

+
+
+ + +
+
+ + +
+ +
No AI summary yet. Use the button above after adding a strong Arabic or English excerpt in the admin upload form.
+ +
+ +
+
Metadata
+

Catalog notes

+
+
Published
+
+
Tags
+
+
Size
+
KB
+
+
+ +
+
Descriptions
+

Source text used by AI

+
+
+
English
+

+
+
+
العربية
+

+
+
+
+
+
+ + + + + + + <?= h($fullTitle) ?> + + + + + + + + + + + + + +
+ + +
+
+ +
+ +
+
+ Library update + +
+
+
+ +
+ + +
+
+ + +
+ + + + + exec($sql); + } + } + + // Run new migrations if needed + $migration2Path = __DIR__ . '/../db/migrations/002_add_library_metadata.sql'; + if (is_file($migration2Path)) { + // Simple check if columns exist + $exists = db()->query("SHOW COLUMNS FROM library_documents LIKE 'category_ar'")->fetch(); + if (!$exists) { + $sql = file_get_contents($migration2Path); + db()->exec($sql); + } + } + + $migration3Path = __DIR__ . '/../db/migrations/003_normalize_categories.sql'; + if (is_file($migration3Path)) { + // Simple check if table exists + $exists = db()->query("SHOW TABLES LIKE 'library_categories'")->fetch(); + if (!$exists) { + $sql = file_get_contents($migration3Path); + db()->exec($sql); + } + } + + $uploadDir = __DIR__ . '/../uploads/library'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0775, true); + } + + library_seed_demo_documents(); + $booted = true; +} + +function h(?string $value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +function library_project_meta(): array +{ + return [ + 'name' => $_SERVER['PROJECT_NAME'] ?? 'Nabd Library', + 'description' => $_SERVER['PROJECT_DESCRIPTION'] ?? 'Bilingual electronic library for Arabic and English documents, online reading, and AI-assisted summaries.', + 'image' => $_SERVER['PROJECT_IMAGE_URL'] ?? '', + ]; +} + +function library_set_flash(string $type, string $message): void +{ + $_SESSION['library_flash'][] = ['type' => $type, 'message' => $message]; +} + +function library_get_flashes(): array +{ + $flashes = $_SESSION['library_flash'] ?? []; + unset($_SESSION['library_flash']); + return is_array($flashes) ? $flashes : []; +} + +function library_seed_demo_documents(): void +{ + $count = (int) (db()->query('SELECT COUNT(*) FROM library_documents')->fetchColumn() ?: 0); + if ($count > 0) { + return; + } + + $pdfRelative = 'uploads/library/demo-library-guide.pdf'; + $txtRelative = 'uploads/library/demo-bilingual-notes.txt'; + $pdfAbsolute = __DIR__ . '/../' . $pdfRelative; + $txtAbsolute = __DIR__ . '/../' . $txtRelative; +} + +function library_old(string $key, string $default = ''): string +{ + return isset($_POST[$key]) ? trim((string) $_POST[$key]) : $default; +} + +function library_document_type_label(string $type): string +{ + $map = [ + 'pdf' => 'PDF reader', + 'txt' => 'Text note', + 'doc' => 'Word document', + 'docx' => 'Word document', + 'ppt' => 'PowerPoint', + 'pptx' => 'PowerPoint', + ]; + return $map[strtolower($type)] ?? strtoupper($type); +} + +function library_language_label(string $lang): string +{ + $map = [ + 'en' => 'English', + 'ar' => 'Arabic', + 'bilingual' => 'Bilingual', + ]; + return $map[$lang] ?? 'Unknown'; +} + +function library_visibility_label(string $visibility): string +{ + return $visibility === 'private' ? 'Private / login' : 'Public'; +} + +function library_allowed_extensions(): array +{ + return [ + 'pdf' => 'PDF reader', + 'txt' => 'Text note', + 'doc' => 'Word document', + 'docx' => 'Word document', + 'ppt' => 'PowerPoint', + 'pptx' => 'PowerPoint', + ]; +} + +// --- Category Functions --- + +function library_get_categories(): array +{ + library_bootstrap(); + $stmt = db()->query('SELECT * FROM library_categories ORDER BY name_en ASC'); + return $stmt ? $stmt->fetchAll() : []; +} + +function library_get_subcategories(?int $categoryId = null): array +{ + library_bootstrap(); + if ($categoryId !== null) { + $stmt = db()->prepare('SELECT * FROM library_subcategories WHERE category_id = ? ORDER BY name_en ASC'); + $stmt->execute([$categoryId]); + return $stmt->fetchAll() ?: []; + } + $stmt = db()->query('SELECT * FROM library_subcategories ORDER BY name_en ASC'); + return $stmt ? $stmt->fetchAll() : []; +} + +function library_create_category(string $nameEn, string $nameAr): int +{ + library_bootstrap(); + $stmt = db()->prepare('INSERT INTO library_categories (name_en, name_ar) VALUES (?, ?)'); + $stmt->execute([$nameEn, $nameAr]); + return (int) db()->lastInsertId(); +} + +function library_create_subcategory(int $categoryId, string $nameEn, string $nameAr): int +{ + library_bootstrap(); + $stmt = db()->prepare('INSERT INTO library_subcategories (category_id, name_en, name_ar) VALUES (?, ?, ?)'); + $stmt->execute([$categoryId, $nameEn, $nameAr]); + return (int) db()->lastInsertId(); +} + +function library_get_category_by_id(int $id): ?array +{ + library_bootstrap(); + $stmt = db()->prepare('SELECT * FROM library_categories WHERE id = ?'); + $stmt->execute([$id]); + return $stmt->fetch() ?: null; +} + +function library_get_subcategory_by_id(int $id): ?array +{ + library_bootstrap(); + $stmt = db()->prepare('SELECT * FROM library_subcategories WHERE id = ?'); + $stmt->execute([$id]); + return $stmt->fetch() ?: null; +} + +// --- End Category Functions --- + +function library_fetch_documents(bool $publicOnly = false, array $filters = []): array +{ + library_bootstrap(); + + $sql = 'SELECT d.*, c.name_en as cat_en, c.name_ar as cat_ar, sc.name_en as sub_en, sc.name_ar as sub_ar + FROM library_documents d + LEFT JOIN library_categories c ON d.category_id = c.id + LEFT JOIN library_subcategories sc ON d.subcategory_id = sc.id + WHERE 1=1'; + $params = []; + + if ($publicOnly) { + $sql .= ' AND d.visibility = :visibility'; + $params[':visibility'] = 'public'; + } + + $sql .= ' ORDER BY d.is_featured DESC, d.created_at DESC'; + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue($key, $value); + } + $stmt->execute(); + + return $stmt->fetchAll() ?: []; +} + +function library_recent_documents(int $limit = 3, bool $publicOnly = false): array +{ + library_bootstrap(); + + $sql = 'SELECT * FROM library_documents WHERE 1=1'; + if ($publicOnly) { + $sql .= ' AND visibility = "public"'; + } + $sql .= ' ORDER BY created_at DESC LIMIT ' . (int)$limit; + + $stmt = db()->query($sql); + return $stmt ? $stmt->fetchAll() : []; +} + +function library_catalog_metrics(): array +{ + library_bootstrap(); + + $sql = 'SELECT + COUNT(*) AS total_count, + SUM(CASE WHEN visibility = "public" THEN 1 ELSE 0 END) AS public_count, + SUM(CASE WHEN visibility = "private" THEN 1 ELSE 0 END) AS private_count, + SUM(CASE WHEN summary_text IS NOT NULL THEN 1 ELSE 0 END) AS summarized_count + FROM library_documents'; + + $row = db()->query($sql)->fetch() ?: []; + + return [ + 'total_count' => (int) ($row['total_count'] ?? 0), + 'public_count' => (int) ($row['public_count'] ?? 0), + 'private_count' => (int) ($row['private_count'] ?? 0), + 'summarized_count' => (int) ($row['summarized_count'] ?? 0), + ]; +} + +function library_handle_uploaded_file(array $file): array +{ + if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + throw new RuntimeException('Please upload a document file.'); + } + + $originalName = (string) ($file['name'] ?? ''); + $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); + $allowed = library_allowed_extensions(); + if (!isset($allowed[$extension])) { + throw new RuntimeException('Unsupported file type.'); + } + + $size = (int) ($file['size'] ?? 0); + if ($size <= 0 || $size > 12 * 1024 * 1024) { + throw new RuntimeException('File must be smaller than 12 MB.'); + } + + $safeBase = preg_replace('/[^a-zA-Z0-9_-]+/', '-', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'document'; + $storedName = strtolower(date('YmdHis') . '-' . $safeBase . '-' . bin2hex(random_bytes(4)) . '.' . $extension); + $relativePath = 'uploads/library/' . $storedName; + $absolutePath = __DIR__ . '/../' . $relativePath; + + if (!move_uploaded_file((string) $file['tmp_name'], $absolutePath)) { + throw new RuntimeException('Unable to save the uploaded file.'); + } + + return [ + 'file_name' => $originalName, + 'file_path' => $relativePath, + 'document_type' => $extension, + 'file_size_kb' => (int) ceil($size / 1024), + ]; +} + +function library_create_document(array $payload, array $file): int +{ + library_bootstrap(); + + $titleEn = trim((string) ($payload['title_en'] ?? '')); + $titleAr = trim((string) ($payload['title_ar'] ?? '')); + + // Process IDs + $categoryId = !empty($payload['category_id']) ? (int)$payload['category_id'] : null; + $subcategoryId = !empty($payload['subcategory_id']) ? (int)$payload['subcategory_id'] : null; + + // Fetch names for backward compatibility if needed, or just store IDs + $categoryName = ''; + $categoryNameAr = ''; + $subName = ''; + $subNameAr = ''; + + if ($categoryId) { + $cat = library_get_category_by_id($categoryId); + if ($cat) { + $categoryName = $cat['name_en']; + $categoryNameAr = $cat['name_ar']; + } + } + if ($subcategoryId) { + $sub = library_get_subcategory_by_id($subcategoryId); + if ($sub) { + $subName = $sub['name_en']; + $subNameAr = $sub['name_ar']; + } + } + + $visibility = (string) ($payload['visibility'] ?? 'public'); + $allow_download = !empty($payload['allow_download']) ? 1 : 0; + $allow_print = !empty($payload['allow_print']) ? 1 : 0; + $allow_copy = !empty($payload['allow_copy']) ? 1 : 0; + + $fileData = library_handle_uploaded_file($file); + + $stmt = db()->prepare('INSERT INTO library_documents ( + title_en, title_ar, + category, category_ar, sub_category, sub_category_ar, + category_id, subcategory_id, + visibility, document_type, + file_name, file_path, file_size_kb, allow_download, allow_print, allow_copy + ) VALUES ( + :title_en, :title_ar, + :category, :category_ar, :sub_category, :sub_category_ar, + :category_id, :subcategory_id, + :visibility, :document_type, + :file_name, :file_path, :file_size_kb, :allow_download, :allow_print, :allow_copy + )'); + + $stmt->execute([ + ':title_en' => $titleEn ?: null, + ':title_ar' => $titleAr ?: null, + ':category' => $categoryName ?: null, + ':category_ar' => $categoryNameAr ?: null, + ':sub_category' => $subName ?: null, + ':sub_category_ar' => $subNameAr ?: null, + ':category_id' => $categoryId, + ':subcategory_id' => $subcategoryId, + ':visibility' => $visibility, + ':document_type' => $fileData['document_type'], + ':file_name' => $fileData['file_name'], + ':file_path' => $fileData['file_path'], + ':file_size_kb' => $fileData['file_size_kb'], + ':allow_download' => $allow_download, + ':allow_print' => $allow_print, + ':allow_copy' => $allow_copy, + ]); + + return (int) db()->lastInsertId(); +} diff --git a/index.php b/index.php index 7205f3d..0103368 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,194 @@ $query, 'language' => $language]); +$metrics = library_catalog_metrics(); +$recentDocuments = library_recent_documents(3, true); + +library_render_header( + 'Digital Catalog', + 'Browse a polished Arabic and English e-library with online reading, public/private publishing controls, and AI-ready summaries.', + 'catalog' +); ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+
+
+ Electronic library · Arabic + English +

A focused e-library for bilingual reading, controlled publishing, and AI-ready summaries.

+

Readers can search the public shelf, open PDFs in-browser, and review concise AI summaries. Your content team can publish titles as public or private from one admin studio.

+ +
+
+
+
+
+
Live shelf snapshot
+

What this first delivery includes

+
+ MVP slice +
+
+
+ + Public titles +
+
+ + Private titles +
+
+ + AI summaries +
+
+
    +
  • Public catalog with search and language filters
  • +
  • Admin upload workflow with visibility control
  • +
  • Document detail view with embedded PDF reader
  • +
+
+
-
- - - + + +
+
+
+
Public discovery
+

Search the live collection

+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
Visibility rules
+

Admin-controlled access

+

Public items appear in this catalog immediately. Private items stay out of the public shelf and are marked for member login in the admin workspace.

+ Review publishing controls +
+
+
+ +
+
+
+
Catalog
+

Available public titles

+
+ result +
+ + +
+
+

No public documents yet

+

Upload your first Arabic or English PDF from the Admin Studio to turn this into a browsable library.

+ Open Admin Studio +
+ +
+ +
+
+
+
+ + +
+ + Featured + +
+
+ +

+ + +
+ +
+
+
Author
+
+
Views
+
+
Tags
+
+
+

+ +
+
+ +
+ +
+ +
+
+
+
Workflow
+

Thin slice, end to end

+
    +
  1. Admin uploads a document and chooses public or private visibility.
  2. +
  3. Readers discover public titles from the catalog and open the detail page.
  4. +
  5. AI summaries can be generated from the saved excerpt for faster review.
  6. +
+
+
+
+
+
+
+
Recently added
+

Latest public titles

+
+ Manage shelf +
+
+ + + +
+
+
+
+> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> +endobj +4 0 obj +<< /Length 263 >> +stream +BT +/F1 18 Tf +60 770 Td +(Nabd Library Demo Guide) Tj +0 -28 Td +/F1 12 Tf +(A bilingual e-library sample for Arabic and English collections.) Tj +0 -24 Td +(This demo PDF proves the in-browser reader workflow is active.) Tj +0 -24 Td +(Upload your own PDF from Admin Studio to replace this starter title.) Tj +0 -24 Td +(Public items appear in the catalog. Private items stay hidden.) Tj +ET +endstream +endobj +5 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> +endobj +xref +0 6 +0000000000 65535 f +0000000009 00000 n +0000000058 00000 n +0000000115 00000 n +0000000241 00000 n +0000000555 00000 n +trailer +<< /Root 1 0 R /Size 6 >> +startxref +625 +%%EOF \ No newline at end of file