Build searchable article library
Deploy GitHub Pages / deploy (push) Canceled after 0s

This commit is contained in:
2026-10-03 02:37:44 +03:00
parent 6c8632315f
commit 9cfffa4c66
4 changed files with 348 additions and 37 deletions
+104 -29
View File
@@ -41,11 +41,23 @@
.topnav nav a:hover, .topnav nav a[aria-current="page"] { color: var(--fg); border-color: var(--accent); }
.nav-action { min-height: 44px; display: inline-flex; align-items: center; padding-inline: 16px; border: 1px solid var(--border); }
.nav-action:hover { border-color: var(--fg); background: var(--fg-soft); }
.hero { padding-block: clamp(64px, 9vw, 112px); border-bottom: 1px solid var(--border); }
.hero { padding-block: clamp(56px, 8vw, 96px); border-bottom: 1px solid var(--border); }
.hero-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(360px, 1.1fr); gap: clamp(36px, 7vw, 88px); align-items: end; }
.eyebrow { margin: 0 0 18px; color: var(--accent); font: 11px/1.5 var(--font-mono); letter-spacing: .06em; }
h1 { max-width: 10ch; font-size: clamp(48px, 7vw, 88px); line-height: 1.06; letter-spacing: -.025em; }
.lead { max-width: 56ch; margin: 24px 0 0; color: var(--muted); font-size: clamp(18px, 2vw, 22px); }
.weekly { display: block; padding: clamp(24px, 4vw, 40px); background: var(--surface); border: 1px solid var(--border); border-top-color: var(--accent); }
.weekly:hover { border-color: var(--fg); background: color-mix(in oklch, var(--surface) 88%, var(--fg)); }
.weekly-meta { display: flex; justify-content: space-between; gap: 16px; color: var(--muted); font: 11px/1.5 var(--font-mono); }
.weekly h2 { margin-top: 42px; font-size: clamp(28px, 4vw, 44px); line-height: 1.18; }
.weekly p { margin: 14px 0 30px; color: var(--muted); }
.weekly-link { min-height: 44px; display: inline-flex; align-items: center; font-weight: 700; border-bottom: 1px solid var(--fg); }
.library { padding-block: clamp(52px, 8vw, 88px); }
.library-tools { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 16px; margin-bottom: 22px; }
.control { width: 100%; min-height: 48px; padding: 11px 14px; border: 1px solid var(--border); background: var(--surface); color: var(--fg); font: inherit; }
.control::placeholder { color: var(--muted); }
.control:hover { border-color: var(--fg); }
.control:focus { outline: 3px solid var(--accent-soft); border-color: var(--accent); }
.filter-panel { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; margin-bottom: 44px; padding-block: 22px; border-block: 1px solid var(--border); }
.filter-group { min-width: 0; }
.filter-label { display: block; margin-bottom: 10px; color: var(--muted); font: 11px/1.5 var(--font-mono); }
@@ -58,14 +70,18 @@
h2 { font-size: clamp(30px, 4vw, 46px); line-height: 1.15; }
.count { color: var(--muted); font: 12px/1.5 var(--font-mono); }
.posts { border-top: 1px solid var(--fg); }
.post { display: grid; grid-template-columns: 138px minmax(0, 1fr) 130px; gap: 28px; align-items: start; padding-block: 28px; border-bottom: 1px solid var(--border); }
.post { display: grid; grid-template-columns: 138px minmax(0, 1fr) 168px; gap: 28px; align-items: start; padding-block: 28px; border-bottom: 1px solid var(--border); }
.post[hidden] { display: none; }
.post-meta { color: var(--muted); font: 12px/1.8 var(--font-mono); }
.post-meta span { display: block; }
.post h3 { font-size: clamp(22px, 2.5vw, 30px); line-height: 1.3; }
.post p { max-width: 62ch; margin: 9px 0 0; color: var(--muted); }
.post-side { justify-self: stretch; display: grid; gap: 12px; }
.access { justify-self: end; display: inline-flex; align-items: center; min-height: 34px; padding: 5px 11px; border: 1px solid var(--border); color: var(--muted); font: 11px/1.5 var(--font-mono); }
.access.free { border-color: var(--accent); color: var(--accent); }
.access.free { border-color: var(--fg); color: var(--fg); }
.engagement { display: flex; justify-content: flex-end; gap: 12px; color: var(--muted); font: 11px/1.5 var(--font-mono); }
.like { min-height: 44px; padding: 0 10px; border: 1px solid transparent; background: transparent; color: var(--muted); }
.like:hover, .like[aria-pressed="true"] { color: var(--fg); border-color: var(--border); }
.post-link { display: inline-flex; align-items: center; min-height: 44px; margin-top: 14px; font-weight: 700; border-bottom: 1px solid transparent; }
.post-link:hover { color: var(--accent); border-color: var(--accent); }
.soon { color: var(--muted); cursor: default; }
@@ -80,17 +96,21 @@
.topnav nav { order: 3; flex: 0 0 calc(100% + (var(--gutter) * 2)); gap: 6px; margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter); overflow-x: auto; border-top: 1px solid var(--border); scrollbar-width: none; }
.topnav nav::-webkit-scrollbar { display: none; }
.topnav nav a { flex: none; min-height: 52px; padding-inline: 12px; white-space: nowrap; }
.filter-panel { grid-template-columns: 1fr; gap: 20px; }
.hero-grid, .filter-panel { grid-template-columns: 1fr; gap: 20px; }
.weekly { margin-top: 8px; }
.post { grid-template-columns: 112px minmax(0, 1fr); }
.access { justify-self: start; grid-column: 2; }
.post-side { justify-self: start; grid-column: 2; }
.access { justify-self: start; }
.engagement { justify-content: flex-start; }
}
@media (max-width: 620px) {
:root { --gutter: 18px; }
.nav-action { padding-inline: 12px; }
h1 { font-size: clamp(42px, 13vw, 58px); }
.library-tools { grid-template-columns: 1fr; }
.results-head { align-items: start; flex-direction: column; }
.post { grid-template-columns: 1fr; gap: 12px; }
.access { grid-column: 1; }
.post-side { grid-column: 1; }
.post-link { width: 100%; border-top: 1px solid var(--border); padding-top: 12px; }
}
@media (prefers-reduced-motion: reduce) {
@@ -108,6 +128,7 @@
<a href="articles.html" aria-current="page" data-od-id="nav-library">المكتبة</a>
<a href="arabic-ai-learning-catalog-v2.html#topics">المسارات</a>
<a href="subscription.html">العضوية</a>
<a href="about.html">عن الفريق</a>
</nav>
<a class="nav-action" href="login.html">تسجيل الدخول</a>
</div>
@@ -115,18 +136,30 @@
<main id="content">
<section class="hero" data-od-id="library-hero">
<div class="container">
<p class="eyebrow">مكتبة أنور بي آي</p>
<h1 data-od-id="library-title">كل الأدلة في مكان واحد.</h1>
<p class="lead">صفِّ المحتوى حسب الفئة ونوع الوصول، ثم افتح المقال المجاني أو معاينة محتوى العضوية.</p>
<div class="container hero-grid">
<div>
<p class="eyebrow">مكتبة أنور بي آي</p>
<h1 data-od-id="library-title">كل المقالات. بلا ضياع.</h1>
<p class="lead">ابحث، اختر مسارك، ورتّب النتائج حسب الأحدث أو التفاعل المسجل على جهازك.</p>
</div>
<a class="weekly" href="article-agent-guide.html" data-od-id="weekly-post">
<div class="weekly-meta"><span>مقال هذا الأسبوع</span><span>مجاني · ١٢ دقيقة</span></div>
<h2>لا تبدأ باسم النموذج؛ ابدأ بطبيعة المهمة</h2>
<p>مصفوفة قرار عملية توازن بين الدقة، السياق، السرعة، والكلفة.</p>
<span class="weekly-link">اقرأ المقال ←</span>
</a>
</div>
</section>
<section class="library" data-od-id="article-library">
<div class="container">
<div class="library-tools" data-od-id="library-search-sort">
<label><span class="filter-label">ابحث في العناوين والوصف</span><input class="control" id="postSearch" type="search" autocomplete="off" placeholder="مثال: الوكلاء، RAG، اختيار النموذج" /></label>
<label><span class="filter-label">الترتيب</span><select class="control" id="postSort"><option value="latest">الأحدث أولاً</option><option value="views">الأكثر مشاهدة على هذا الجهاز</option><option value="likes">الأكثر إعجاباً على هذا الجهاز</option></select></label>
</div>
<div class="filter-panel" data-od-id="library-filters">
<div class="filter-group" role="group" aria-label="تصفية حسب الفئة">
<span class="filter-label">الفئة</span>
<div class="filter-group" role="group" aria-label="تصفية حسب المسار">
<span class="filter-label">المسار / الفئة</span>
<div class="filters">
<button class="filter" type="button" data-filter-kind="topic" data-filter-value="all" aria-pressed="true">الكل</button>
<button class="filter" type="button" data-filter-kind="topic" data-filter-value="agents" aria-pressed="false">الوكلاء</button>
@@ -151,43 +184,43 @@
</div>
<div class="posts" id="postList">
<article class="post" data-topic="agents" data-access="vip" data-od-id="post-agent-loop">
<article class="post" data-topic="agents" data-access="vip" data-order="6" data-id="agent-loop" data-od-id="post-agent-loop">
<div class="post-meta"><span>الوكلاء</span><span>١٨ دقيقة</span></div>
<div><h3>البنية التي تمنع وكيلك من الدوران في حلقة</h3><p>حدود واضحة بين التخطيط والتنفيذ والتحقق، مع نموذج بسيط لسجل الحالة.</p><a class="post-link" href="post-locked.html">شاهد المعاينة ←</a></div>
<span class="access">VIP · معاينة</span>
<div class="post-side"><span class="access">VIP · معاينة</span><div class="engagement"><span data-views>٠ مشاهدة</span><button class="like" type="button" data-like aria-pressed="false">إعجاب · <span>٠</span></button></div></div>
</article>
<article class="post" data-topic="models" data-access="free" data-od-id="post-model-selection">
<article class="post" data-topic="models" data-access="free" data-order="5" data-id="model-selection" data-od-id="post-model-selection">
<div class="post-meta"><span>النماذج</span><span>١٢ دقيقة</span></div>
<div><h3>لا تبدأ باسم النموذج؛ ابدأ بطبيعة المهمة</h3><p>مصفوفة قرار عملية توازن بين الدقة، السياق، السرعة، والكلفة.</p><a class="post-link" href="article-agent-guide.html">ابدأ القراءة ←</a></div>
<span class="access free">مجاني · كامل</span>
<div class="post-side"><span class="access free">مجاني · كامل</span><div class="engagement"><span data-views>٠ مشاهدة</span><button class="like" type="button" data-like aria-pressed="false">إعجاب · <span>٠</span></button></div></div>
</article>
<article class="post" data-topic="automation" data-access="vip" data-od-id="post-automation-observability">
<article class="post" data-topic="automation" data-access="vip" data-order="4" data-id="automation-observability" data-od-id="post-automation-observability">
<div class="post-meta"><span>الأتمتة</span><span>٢٤ دقيقة</span></div>
<div><h3>كيف تكتشف فشل سير العمل قبل المستخدم؟</h3><p>إشارات قليلة تكفي لمراقبة التكلفة، التأخير، ونقاط الانقطاع.</p><span class="post-link soon" aria-label="هذه المادة ستتوفر قريباً">قريباً</span></div>
<span class="access">VIP</span>
<div class="post-side"><span class="access">VIP</span><div class="engagement"><span data-views>٠ مشاهدة</span><button class="like" type="button" data-like aria-pressed="false">إعجاب · <span>٠</span></button></div></div>
</article>
<article class="post" data-topic="foundations" data-access="free" data-od-id="post-prompt-context">
<article class="post" data-topic="foundations" data-access="free" data-order="3" data-id="prompt-context" data-od-id="post-prompt-context">
<div class="post-meta"><span>الأساسيات</span><span>٩ دقائق</span></div>
<div><h3>لماذا لا يصلح تحسين الصياغة بياناتك الرديئة؟</h3><p>افصل بين التعليمات، المعرفة، وذاكرة المحادثة لتحصل على نتيجة مستقرة.</p><span class="post-link soon" aria-label="هذه المادة ستتوفر قريباً">قريباً</span></div>
<span class="access free">مجاني</span>
<div class="post-side"><span class="access free">مجاني</span><div class="engagement"><span data-views>٠ مشاهدة</span><button class="like" type="button" data-like aria-pressed="false">إعجاب · <span>٠</span></button></div></div>
</article>
<article class="post" data-topic="agents" data-access="vip" data-od-id="post-tool-safety">
<article class="post" data-topic="agents" data-access="vip" data-order="2" data-id="tool-safety" data-od-id="post-tool-safety">
<div class="post-meta"><span>الوكلاء</span><span>١٥ دقيقة</span></div>
<div><h3>قبل أن تمنح الوكيل صلاحية التنفيذ</h3><p>حدود الصلاحيات، التأكيد البشري، وسجل تدقيق يمكنك الرجوع إليه.</p><span class="post-link soon" aria-label="هذه المادة ستتوفر قريباً">قريباً</span></div>
<span class="access">VIP</span>
<div class="post-side"><span class="access">VIP</span><div class="engagement"><span data-views>٠ مشاهدة</span><button class="like" type="button" data-like aria-pressed="false">إعجاب · <span>٠</span></button></div></div>
</article>
<article class="post" data-topic="models" data-access="vip" data-od-id="post-rag-evaluation">
<article class="post" data-topic="models" data-access="vip" data-order="1" data-id="rag-evaluation" data-od-id="post-rag-evaluation">
<div class="post-meta"><span>النماذج</span><span>٤ أجزاء</span></div>
<div><h3>اختبر الاسترجاع والإجابة كلّاً على حدة</h3><p>منهج تقييم يفصل فشل المصدر عن فشل النموذج ويقودك إلى إصلاح صحيح.</p><span class="post-link soon" aria-label="هذه المادة ستتوفر قريباً">قريباً</span></div>
<span class="access">VIP</span>
<div class="post-side"><span class="access">VIP</span><div class="engagement"><span data-views>٠ مشاهدة</span><button class="like" type="button" data-like aria-pressed="false">إعجاب · <span>٠</span></button></div></div>
</article>
</div>
<p class="empty" id="emptyState">لا توجد مواد تطابق هذين الفلترين.</p>
<p class="empty" id="emptyState">لا توجد مواد تطابق البحث والفلاتر الحالية.</p>
</div>
</section>
</main>
@@ -198,22 +231,48 @@
<script>
(() => {
const state = { topic: 'all', access: 'all' };
const params = new URLSearchParams(location.search);
const allowedTopics = ['all', 'agents', 'models', 'automation', 'foundations'];
const state = { topic: allowedTopics.includes(params.get('topic')) ? params.get('topic') : 'all', access: 'all', query: '', sort: 'latest' };
const buttons = [...document.querySelectorAll('[data-filter-kind]')];
const posts = [...document.querySelectorAll('.post')];
const list = document.getElementById('postList');
const count = document.getElementById('resultCount');
const empty = document.getElementById('emptyState');
const arabicCounts = ['٠', '١', '٢', '٣', '٤', '٥', '٦'];
const search = document.getElementById('postSearch');
const sort = document.getElementById('postSort');
const formatNumber = value => new Intl.NumberFormat('ar').format(value);
const readMetric = (post, metric) => Number(localStorage.getItem(`anwarpi:${metric}:${post.dataset.id}`) || 0);
function normalize(value) {
return value.toLocaleLowerCase('ar').normalize('NFKD').replace(/[\u064B-\u065F\u0670]/g, '').replace(/[إأآ]/g, 'ا').replace(/ى/g, 'ي');
}
function paintMetrics(post) {
const views = readMetric(post, 'views');
const liked = localStorage.getItem(`anwarpi:liked:${post.dataset.id}`) === '1';
post.querySelector('[data-views]').textContent = `${formatNumber(views)} مشاهدة`;
const like = post.querySelector('[data-like]');
like.setAttribute('aria-pressed', String(liked));
like.querySelector('span').textContent = formatNumber(liked ? 1 : 0);
}
function applyFilters() {
const ordered = [...posts].sort((a, b) => {
if (state.sort === 'views') return readMetric(b, 'views') - readMetric(a, 'views') || Number(b.dataset.order) - Number(a.dataset.order);
if (state.sort === 'likes') return Number(localStorage.getItem(`anwarpi:liked:${b.dataset.id}`) === '1') - Number(localStorage.getItem(`anwarpi:liked:${a.dataset.id}`) === '1') || Number(b.dataset.order) - Number(a.dataset.order);
return Number(b.dataset.order) - Number(a.dataset.order);
});
ordered.forEach(post => list.appendChild(post));
let visible = 0;
posts.forEach(post => {
const topicMatch = state.topic === 'all' || post.dataset.topic === state.topic;
const accessMatch = state.access === 'all' || post.dataset.access === state.access;
post.hidden = !(topicMatch && accessMatch);
const queryMatch = !state.query || normalize(post.textContent).includes(normalize(state.query));
post.hidden = !(topicMatch && accessMatch && queryMatch);
if (!post.hidden) visible += 1;
});
count.textContent = `${arabicCounts[visible]} ${visible === 1 ? 'مادة' : 'مواد'}`;
count.textContent = `${formatNumber(visible)} ${visible === 1 ? 'مادة' : 'مواد'}`;
empty.classList.toggle('show', visible === 0);
}
@@ -226,6 +285,22 @@
applyFilters();
}));
search.addEventListener('input', () => { state.query = search.value.trim(); applyFilters(); });
sort.addEventListener('change', () => { state.sort = sort.value; applyFilters(); });
posts.forEach(post => {
paintMetrics(post);
post.querySelector('[data-like]').addEventListener('click', () => {
const key = `anwarpi:liked:${post.dataset.id}`;
localStorage.setItem(key, localStorage.getItem(key) === '1' ? '0' : '1');
paintMetrics(post);
applyFilters();
});
post.querySelectorAll('a.post-link').forEach(link => link.addEventListener('click', () => {
localStorage.setItem(`anwarpi:views:${post.dataset.id}`, String(readMetric(post, 'views') + 1));
}));
});
buttons.filter(button => button.dataset.filterKind === 'topic').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.filterValue === state.topic)));
applyFilters();
})();
</script>