صمم لي برنامج يخص الختمات القرائنية
创建于:2026年4月3日
使用 GPT-5.4 Thinking 回答 Chat01
创建于:2026年4月3日
使用 GPT-5.4 Thinking 回答 Chat01
صمم لي برنامج يخص الختمات القرائنية
Designing a Quran completion program
The user's request appears to involve designing a program related to Quran reading completions, possibly an app. Since the task is unclear, I'll provide a simple conceptual design in Arabic, focusing on the idea, features, screens, database, user flow, and MVP (minimum viable product). No web integration is needed for this basic request.
Thought for 10s
أكيد — هذا تصور جاهز لبرنامج خاص بالختمات القرآنية، بشكل عملي وواضح:
ختمتي
تطبيق يساعد المستخدم على:
تسهيل ختم القرآن بطريقة منظمة ومشجعة، سواء في:
عند بدء الختمة يختار المستخدم:
مثلاً:
يعرض للمستخدم:
إشعارات مثل:
في الختمة الجماعية يمكن:
صفحة تعرض:
تحتوي على:
لزيادة التحفيز:
ابدأ رحلتك مع القرآن بخطة واضحة وإنجاز يومي بسيط.
حدد مدة الختمة، واترك علينا تنظيم وردك اليومي.
لو أردته كنسخة أولى فقط، فتكفي هذه الميزات:
أستطيع أن أحوّل هذا الآن إلى مخطط واجهات شاشة بشاشة أو وثيقة مشروع كاملة أو كود مبدئي للتطبيق.
حوله الى تطبيق يعمل على الجوال والحاسب الالي
Designing a Quran completion program
The user's request appears to involve designing a program related to Quran reading completions, possibly an app. Since the task is unclear, I'll provide a simple conceptual design in Arabic, focusing on the idea, features, screens, database, user flow, and MVP (minimum viable product). No web integration is needed for this basic request.Creating a cross-platform app prototype
The user wants an app that works on both mobile and desktop. It seems like I should create a cross-platform solution, like a responsive web app or a PWA. The easiest route might be generating an app using HTML/CSS/JS for both platforms and packaging it later with something like Electron or Capacitor.Creating app files for MVP
I should create the necessary app files: index.html, style.css, and app.js, and maybe add manifest.json and a service worker for PWA functionality. The app will include features like a daily plan, group mock, progress tracking, Arabic RTL UI, and tabs for different sections. It’ll be an MVP for mobile and desktop, with the option to distribute later. I'll package the files in a downloadable folder or zip.
{"path":"skills://","only_tools":false}
Building a responsive app with key features
I’ll create a responsive app with the following features: custom plan creation, daily assignments, page distribution, and progress tracking using localStorage. I'll include summaries, statistics, a progress bar, and a history of completed plans. It’ll be installable as a PWA with Arabic RTL support and maybe a dark mode toggle. Group mode could be added later. I’ll organize the structure well and include a README, too.
Adding group features as a local tool
I’ll add a simple group feature where users can create a group and auto-assign 30 juz to participants as a local planning tool. It will track completion and show progress within the app, stored in localStorage. I’ll make sure to ZIP the project folder for easy distribution and draft the code carefully. I'll keep everything in ASCII format for compatibility.Deciding on code file generation method
I’m considering different ways to generate code files. It seems like the easiest approach might be using a method that outputs Python code directly, like through a "visible" output. I can use this to create files, but I need to make sure the solution is user-friendly. I’ll stick with that approach.
from pathlib import Path import json, zipfile, textwrap, os
base = Path("/mnt/data/khatmati_app") base.mkdir(exist_ok=True)
index_html = r'''<!DOCTYPE html>
<html lang="ar" dir="rtl"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ختمتي</title> <meta name="theme-color" content="#14532d" /> <meta name="description" content="تطبيق متابعة الختمات القرآنية على الجوال والحاسب" /> <link rel="manifest" href="manifest.json" /> <link rel="stylesheet" href="styles.css" /> </head> <body> <div class="app-shell"> <aside class="sidebar"> <div class="brand"> <div class="brand-badge">📖</div> <div> <h1>ختمتي</h1> <p>متابعة الختمات القرآنية</p> </div> </div></div> <div id="toast" class="toast"></div> <script src="app.js"></script> </body> </html> '''text<nav class="nav"> <button class="nav-btn active" data-tab="home">الرئيسية</button> <button class="nav-btn" data-tab="new-khatma">ختمة جديدة</button> <button class="nav-btn" data-tab="plan">الخطة اليومية</button> <button class="nav-btn" data-tab="group">الختمة الجماعية</button> <button class="nav-btn" data-tab="stats">الإحصائيات</button> </nav> <div class="sidebar-card"> <h3>مميزات النسخة الحالية</h3> <ul> <li>إنشاء خطة ختمة تلقائيًا</li> <li>حفظ التقدم على الجهاز</li> <li>حساب الورد اليومي</li> <li>متابعة فردية وجماعية</li> <li>يعمل على الجوال والحاسب</li> </ul> </div> </aside> <main class="content"> <section id="home" class="tab active"> <div class="hero"> <div> <span class="pill">تطبيق متجاوب + قابل للتثبيت</span> <h2>ابدأ ختمتك بخطة واضحة وإنجاز يومي بسيط</h2> <p> حدّد مدة الختمة، وسيقوم التطبيق بتوزيع الصفحات أو الأجزاء تلقائيًا، ثم تابع تقدمك يومًا بيوم. </p> </div> <button id="quickStartBtn" class="primary-btn">ابدأ الآن</button> </div> <div id="currentKhatmaCard" class="grid two"></div> </section> <section id="new-khatma" class="tab"> <div class="section-header"> <h2>إنشاء ختمة جديدة</h2> <p>أدخل بيانات الختمة وسيتم توليد الخطة اليومية تلقائيًا.</p> </div> <form id="khatmaForm" class="card form-grid"> <label> <span>اسم الختمة</span> <input type="text" id="title" placeholder="مثل: ختمة رمضان" required /> </label> <label> <span>نوع الختمة</span> <select id="type"> <option value="individual">فردية</option> <option value="group">جماعية</option> </select> </label> <label> <span>مدة الختمة</span> <select id="durationPreset"> <option value="7">7 أيام</option> <option value="15">15 يومًا</option> <option value="30" selected>30 يومًا</option> <option value="custom">مدة مخصصة</option> </select> </label> <label id="customDaysWrap" class="hidden"> <span>عدد الأيام</span> <input type="number" id="customDays" min="1" max="365" value="30" /> </label> <label> <span>بداية القراءة</span> <select id="startFrom"> <option value="1">من بداية المصحف</option> <option value="custom">من صفحة مخصصة</option> </select> </label> <label id="customPageWrap" class="hidden"> <span>صفحة البداية</span> <input type="number" id="customStartPage" min="1" max="604" value="1" /> </label> <label> <span>طريقة التقسيم</span> <select id="unit"> <option value="pages">بالصفحات</option> <option value="juz">بالأجزاء</option> <option value="hizb">بالأحزاب</option> </select> </label> <label> <span>تاريخ البداية</span> <input type="date" id="startDate" required /> </label> <div class="form-actions"> <button type="submit" class="primary-btn">حفظ الختمة وتوليد الخطة</button> </div> </form> </section> <section id="plan" class="tab"> <div class="section-header"> <h2>الخطة اليومية</h2> <p>تابع إنجازك اليومي وحدّث حالة كل ورد.</p> </div> <div id="planSummary" class="grid three"></div> <div id="planList" class="list"></div> </section> <section id="group" class="tab"> <div class="section-header"> <h2>الختمة الجماعية</h2> <p>وزّع الأجزاء على المشاركين وتابع إنجاز المجموعة.</p> </div> <form id="groupForm" class="card form-grid"> <label> <span>اسم المجموعة</span> <input type="text" id="groupName" placeholder="مثل: ختمة العائلة" required /> </label> <label class="full"> <span>أسماء المشاركين</span> <textarea id="participants" rows="4" placeholder="أحمد، فاطمة، سارة، محمد"></textarea> </label> <div class="form-actions"> <button type="submit" class="primary-btn">توزيع 30 جزءًا تلقائيًا</button> </div> </form> <div id="groupSummary" class="grid three"></div> <div id="groupAssignments" class="list"></div> </section> <section id="stats" class="tab"> <div class="section-header"> <h2>الإحصائيات</h2> <p>نظرة سريعة على إنجازك الحالي والختمات السابقة.</p> </div> <div id="statsGrid" class="grid four"></div> <div class="card"> <h3>سجل الختمات</h3> <div id="historyList" class="history-list"></div> </div> </section> </main>
styles_css = r'''* { box-sizing: border-box; }
:root { --bg: #f5f7f2; --card: #ffffff; --text: #18221b; --muted: #5f6d61; --line: #dfe7de; --primary: #14532d; --primary-soft: #e8f4ea; --gold: #b88a1b; --danger: #b42318; --shadow: 0 10px 30px rgba(20, 83, 45, 0.08); }
body { margin: 0; font-family: "Segoe UI", Tahoma, Arial, sans-serif; background: linear-gradient(180deg, #f6f8f4 0%, #edf5ef 100%); color: var(--text); }
.app-shell { min-height: 100vh; display: grid; grid-template-columns: 290px 1fr; }
.sidebar { padding: 24px; background: #113d23; color: #fff; display: flex; flex-direction: column; gap: 22px; position: sticky; top: 0; min-height: 100vh; }
.brand { display: flex; align-items: center; gap: 14px; }
.brand-badge { width: 52px; height: 52px; border-radius: 16px; display: grid; place-items: center; background: rgba(255,255,255,0.12); font-size: 28px; }
.brand h1 { margin: 0 0 4px; font-size: 1.6rem; }
.brand p { margin: 0; color: rgba(255,255,255,0.75); font-size: 0.95rem; }
.nav { display: grid; gap: 10px; }
.nav-btn { border: 0; background: transparent; color: rgba(255,255,255,0.92); text-align: right; padding: 12px 14px; border-radius: 14px; cursor: pointer; transition: 0.2s ease; font-size: 1rem; }
.nav-btn:hover, .nav-btn.active { background: rgba(255,255,255,0.12); }
.sidebar-card { margin-top: auto; background: rgba(255,255,255,0.09); padding: 16px; border-radius: 18px; }
.sidebar-card h3 { margin-top: 0; font-size: 1rem; }
.sidebar-card ul { margin: 0; padding-right: 18px; line-height: 1.9; color: rgba(255,255,255,0.85); }
.content { padding: 28px; }
.tab { display: none; }
.tab.active { display: block; }
.hero { background: radial-gradient(circle at top right, #eaf5ec, #ffffff 65%); border: 1px solid var(--line); box-shadow: var(--shadow); border-radius: 26px; padding: 24px; display: flex; align-items: center; justify-content: space-between; gap: 22px; margin-bottom: 22px; }
.hero h2 { margin: 12px 0 8px; font-size: 2rem; }
.hero p { margin: 0; max-width: 720px; color: var(--muted); line-height: 1.8; }
.pill { display: inline-block; background: var(--primary-soft); color: var(--primary); padding: 8px 14px; border-radius: 999px; font-size: 0.9rem; font-weight: 700; }
.primary-btn { background: var(--primary); color: #fff; border: 0; border-radius: 16px; padding: 14px 18px; font-size: 1rem; cursor: pointer; box-shadow: var(--shadow); }
.primary-btn:hover { opacity: 0.95; }
.section-header { margin-bottom: 18px; }
.section-header h2 { margin-bottom: 8px; }
.section-header p { margin: 0; color: var(--muted); }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 24px; padding: 20px; box-shadow: var(--shadow); }
.grid { display: grid; gap: 16px; }
.grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.metric-card { background: var(--card); border: 1px solid var(--line); border-radius: 22px; padding: 18px; box-shadow: var(--shadow); }
.metric-card .label { color: var(--muted); margin-bottom: 8px; }
.metric-card .value { font-size: 1.55rem; font-weight: 700; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.form-grid label { display: grid; gap: 8px; }
.form-grid label.full { grid-column: 1 / -1; }
.form-actions { grid-column: 1 / -1; display: flex; justify-content: flex-start; margin-top: 4px; }
input, select, textarea { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 12px 14px; font: inherit; color: var(--text); }
input:focus, select:focus, textarea:focus { outline: 3px solid rgba(20,83,45,0.12); border-color: var(--primary); }
.list { display: grid; gap: 14px; margin-top: 16px; }
.day-item, .assignment-item, .empty-state { background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 16px; box-shadow: var(--shadow); }
.day-item { display: grid; gap: 12px; }
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.badges { display: flex; gap: 8px; flex-wrap: wrap; }
.badge { background: var(--primary-soft); color: var(--primary); border-radius: 999px; padding: 7px 12px; font-size: 0.87rem; font-weight: 600; }
.badge.gold { background: #fff7e1; color: var(--gold); }
.badge.done { background: #ebf8ee; color: #1d7f3f; }
.badge.pending { background: #fff2f1; color: var(--danger); }
.progress-wrap { background: #edf1ed; height: 14px; border-radius: 999px; overflow: hidden; }
.progress-bar { height: 100%; background: linear-gradient(90deg, #14532d, #2b7a43); border-radius: 999px; }
.checkbox-row { display: flex; align-items: center; gap: 10px; color: var(--muted); }
.checkbox-row input { width: 18px; height: 18px; }
.history-list { display: grid; gap: 12px; }
.history-item { border: 1px dashed var(--line); border-radius: 16px; padding: 14px; }
.hidden { display: none !important; }
.toast { position: fixed; left: 24px; bottom: 24px; background: #183b26; color: #fff; padding: 12px 16px; border-radius: 14px; opacity: 0; transform: translateY(15px); pointer-events: none; transition: 0.25s ease; box-shadow: var(--shadow); z-index: 999; }
.toast.show { opacity: 1; transform: translateY(0); }
.empty-state { text-align: center; color: var(--muted); }
@media (max-width: 980px) { .app-shell { grid-template-columns: 1fr; }
.sidebar { min-height: auto; position: static; border-bottom-left-radius: 28px; border-bottom-right-radius: 28px; }
.grid.two, .grid.three, .grid.four, .form-grid { grid-template-columns: 1fr; }
.hero { flex-direction: column; align-items: flex-start; }
.content { padding: 18px; } } '''
app_js = r'''const STORAGE_KEY = "khatmati_data_v1";
const defaultData = { currentKhatma: null, history: [], groupKhatma: null };
function getTodayISO() { const date = new Date(); const tzOffset = date.getTimezoneOffset() * 60000; return new Date(date - tzOffset).toISOString().slice(0, 10); }
function loadData() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || structuredClone(defaultData); } catch { return structuredClone(defaultData); } }
function saveData() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
function showToast(message) { const toast = document.getElementById("toast"); toast.textContent = message; toast.classList.add("show"); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove("show"), 2500); }
function switchTab(tabId) {
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".nav-btn").forEach(b => b.classList.remove("active"));
document.getElementById(tabId).classList.add("active");
document.querySelector(.nav-btn[data-tab="${tabId}"]).classList.add("active");
}
function formatDate(dateStr) { const d = new Date(dateStr); if (Number.isNaN(d.getTime())) return dateStr; return d.toLocaleDateString("ar-EG", { year: "numeric", month: "long", day: "numeric" }); }
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function calculatePagesPerDay(startPage, days) { const totalPages = 605 - startPage; const base = Math.floor(totalPages / days); const remainder = totalPages % days; const plan = []; let cursor = startPage;
for (let day = 1; day <= days; day++) { const pagesToday = base + (day <= remainder ? 1 : 0); const from = cursor; const to = clamp(cursor + pagesToday - 1, 1, 604); plan.push({ day, fromPage: from, toPage: to, completed: false }); cursor = to + 1; } return plan; }
function pagesToJuz(fromPage, toPage) {
const pagePerJuz = 604 / 30;
const startJuz = Math.ceil(fromPage / pagePerJuz);
const endJuz = Math.ceil(toPage / pagePerJuz);
return startJuz === endJuz ? الجزء ${startJuz} : من الجزء ${startJuz} إلى ${endJuz};
}
function pagesToHizb(fromPage, toPage) {
const pagePerHizb = 604 / 60;
const start = Math.ceil(fromPage / pagePerHizb);
const end = Math.ceil(toPage / pagePerHizb);
return start === end ? الحزب ${start} : من الحزب ${start} إلى ${end};
}
function describeUnit(unit, fromPage, toPage) {
if (unit === "juz") return pagesToJuz(fromPage, toPage);
if (unit === "hizb") return pagesToHizb(fromPage, toPage);
return من صفحة ${fromPage} إلى ${toPage};
}
function createKhatma(payload) { const existing = state.currentKhatma; if (existing) { state.history.unshift({ title: existing.title, type: existing.type, startDate: existing.startDate, endDate: existing.endDate, completedDays: existing.plan.filter(d => d.completed).length, totalDays: existing.days, progress: calculateProgress(existing.plan), archivedAt: getTodayISO() }); }
const plan = calculatePagesPerDay(payload.startPage, payload.days); state.currentKhatma = { ...payload, plan, createdAt: getTodayISO(), endDate: addDays(payload.startDate, payload.days - 1) }; saveData(); renderAll(); showToast("تم إنشاء الختمة بنجاح"); switchTab("plan"); }
function addDays(dateStr, days) { const d = new Date(dateStr); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); }
function calculateProgress(plan) { if (!plan?.length) return 0; return Math.round((plan.filter(day => day.completed).length / plan.length) * 100); }
function currentDayNumber(startDate, totalDays) { const today = new Date(getTodayISO()); const start = new Date(startDate); const diff = Math.floor((today - start) / (1000 * 60 * 60 * 24)) + 1; return clamp(diff, 1, totalDays); }
function completedPages(plan) { return plan .filter(item => item.completed) .reduce((sum, item) => sum + (item.toPage - item.fromPage + 1), 0); }
function createMetric(label, value) {
return <div class="metric-card"><div class="label">${label}</div><div class="value">${value}</div></div>;
}
function renderHome() {
const target = document.getElementById("currentKhatmaCard");
if (!state.currentKhatma) {
target.innerHTML = <div class="empty-state"> <h3>لا توجد ختمة نشطة حاليًا</h3> <p>انتقل إلى تبويب "ختمة جديدة" وابدأ خطتك الأولى.</p> </div> ;
return;
}
const k = state.currentKhatma; const progress = calculateProgress(k.plan); const todayDay = currentDayNumber(k.startDate, k.days); const todayPlan = k.plan[todayDay - 1] || k.plan[0]; const completed = k.plan.filter(d => d.completed).length;
target.innerHTML = ` <div class="card"> <div class="row"> <div> <h3>k.title</h3><p>{k.type === "group" ? "ختمة جماعية" : "ختمة فردية"} • من formatDate(k.startDate)إلى{formatDate(k.endDate)}</p> </div> <div class="badges"> <span class="badge gold">progress<spanclass="badge">اليوم{todayDay} من k.days</span></div></div><divclass="progress−wrap"><divclass="progress−bar"style="width:{progress}%"></div></div> <div class="row"> <span>الأيام المكتملة: completed/{k.days}</span> <span>الصفحات المنجزة: completedPages(k.plan)/{605 - k.startPage}</span> </div> </div>
text<div class="card"> <h3>ورد اليوم</h3> <p>${describeUnit(k.unit, todayPlan.fromPage, todayPlan.toPage)}</p> <div class="badges"> <span class="badge">صفحة البداية: ${todayPlan.fromPage}</span> <span class="badge">صفحة النهاية: ${todayPlan.toPage}</span> <span class="badge ${todayPlan.completed ? "done" : "pending"}">${todayPlan.completed ? "مكتمل" : "بانتظار الإنجاز"}</span> </div> </div>
`; }
function renderPlan() { const summary = document.getElementById("planSummary"); const list = document.getElementById("planList");
if (!state.currentKhatma) {
summary.innerHTML = "";
list.innerHTML = <div class="empty-state">أنشئ ختمة جديدة لتظهر الخطة هنا.</div>;
return;
}
const k = state.currentKhatma; const progress = calculateProgress(k.plan); const done = k.plan.filter(d => d.completed).length; const remaining = k.days - done; const todayDay = currentDayNumber(k.startDate, k.days);
summary.innerHTML = ${createMetric("نسبة الإنجاز",progress{createMetric("الأيام المكتملة", ${done} / ${k.days})}
{createMetric("الأيام المتبقية", `{remaining})} ;
list.innerHTML = k.plan.map(day => <div class="day-item"> <div class="row"> <strong>اليوم ${day.day}</strong> <div class="badges"> ${day.day === todayDay ?<span class="badge gold">ورد اليوم</span>: ``} <span class="badge ${day.completed ? "done" : "pending"}">${day.completed ? "مكتمل" : "لم يكتمل بعد"}</span> </div> </div> <div class="row"> <span>${describeUnit(k.unit, day.fromPage, day.toPage)}</span> <span>من ${formatDate(addDays(k.startDate, day.day - 1))}</span> </div> <label class="checkbox-row"> <input type="checkbox" ${day.completed ? "checked" : ""} onchange="toggleDayCompletion(${day.day})" /> <span>تم إنجاز ورد هذا اليوم</span> </label> </div> ).join("");
}
function toggleDayCompletion(dayNumber) { const item = state.currentKhatma.plan.find(day => day.day === dayNumber); item.completed = !item.completed; saveData(); renderAll(); if (calculateProgress(state.currentKhatma.plan) === 100) { state.history.unshift({ title: state.currentKhatma.title, type: state.currentKhatma.type, startDate: state.currentKhatma.startDate, endDate: state.currentKhatma.endDate, completedDays: state.currentKhatma.days, totalDays: state.currentKhatma.days, progress: 100, archivedAt: getTodayISO(), status: "completed" }); showToast("مبارك عليك إتمام الختمة"); } else { showToast("تم تحديث التقدم"); } }
function createGroupKhatma(groupName, participantsText) { const participants = participantsText .split(/[,\n،]/) .map(name => name.trim()) .filter(Boolean);
if (!participants.length) { showToast("أدخل أسماء المشاركين أولًا"); return; }
const assignments = []; for (let juz = 1; juz <= 30; juz++) { assignments.push({ juz, participant: participants[(juz - 1) % participants.length], completed: false }); }
state.groupKhatma = { groupName, participants, assignments, createdAt: getTodayISO() }; saveData(); renderGroup(); showToast("تم توزيع الأجزاء على المجموعة"); }
function renderGroup() { const summary = document.getElementById("groupSummary"); const list = document.getElementById("groupAssignments");
if (!state.groupKhatma) {
summary.innerHTML = "";
list.innerHTML = <div class="empty-state">أنشئ ختمة جماعية لتظهر التوزيعات هنا.</div>;
return;
}
const g = state.groupKhatma; const done = g.assignments.filter(a => a.completed).length; const progress = Math.round((done / g.assignments.length) * 100);
summary.innerHTML = ${createMetric("اسم المجموعة", g.groupName)} ${createMetric("عدد المشاركين", g.participants.length)} ${createMetric("نسبة الإنجاز",${progress}%)} ;
list.innerHTML = g.assignments.map(a => <div class="assignment-item"> <div class="row"> <strong>الجزء ${a.juz}</strong> <span class="badge">${a.participant}</span> </div> <label class="checkbox-row"> <input type="checkbox" ${a.completed ? "checked" : ""} onchange="toggleGroupAssignment(${a.juz})" /> <span>${a.completed ? "تم إكمال هذا الجزء" : "لم يكتمل بعد"}</span> </label> </div> ).join("");
}
function toggleGroupAssignment(juz) { const item = state.groupKhatma.assignments.find(a => a.juz === juz); item.completed = !item.completed; saveData(); renderGroup(); showToast("تم تحديث الختمة الجماعية"); }
function renderStats() { const statsGrid = document.getElementById("statsGrid"); const historyList = document.getElementById("historyList");
const current = state.currentKhatma; const activeProgress = current ? calculateProgress(current.plan) : 0; const activeDone = current ? current.plan.filter(d => d.completed).length : 0; const totalHistory = state.history.length; const completedHistory = state.history.filter(item => item.progress === 100 || item.status === "completed").length;
statsGrid.innerHTML = ${createMetric("الختمات المحفوظة", totalHistory)} ${createMetric("الختمات المكتملة", completedHistory)} ${createMetric("إنجاز الختمة الحالية",activeProgress{createMetric("أيام منجزة حاليًا", current ? ${activeDone}/${current.days} : "0")}
`;
if (!state.history.length) {
historyList.innerHTML = <div class="empty-state">لا يوجد سجل محفوظ بعد.</div>;
return;
}
historyList.innerHTML = state.history.map(item => <div class="history-item"> <div class="row"> <strong>${item.title}</strong> <span class="badge ${item.progress === 100 ? "done" : "pending"}">${item.progress}%</span> </div> <div class="row"> <span>${item.type === "group" ? "جماعية" : "فردية"}</span> <span>${formatDate(item.startDate)} - ${formatDate(item.endDate)}</span> </div> </div> ).join("");
}
function renderAll() { renderHome(); renderPlan(); renderGroup(); renderStats(); }
const state = loadData();
document.querySelectorAll(".nav-btn").forEach(btn => { btn.addEventListener("click", () => switchTab(btn.dataset.tab)); });
document.getElementById("quickStartBtn").addEventListener("click", () => switchTab("new-khatma"));
const durationPreset = document.getElementById("durationPreset"); const customDaysWrap = document.getElementById("customDaysWrap"); durationPreset.addEventListener("change", () => { customDaysWrap.classList.toggle("hidden", durationPreset.value !== "custom"); });
const startFrom = document.getElementById("startFrom"); const customPageWrap = document.getElementById("customPageWrap"); startFrom.addEventListener("change", () => { customPageWrap.classList.toggle("hidden", startFrom.value !== "custom"); });
document.getElementById("startDate").value = getTodayISO();
document.getElementById("khatmaForm").addEventListener("submit", (e) => { e.preventDefault();
const title = document.getElementById("title").value.trim(); const type = document.getElementById("type").value; const unit = document.getElementById("unit").value; const preset = durationPreset.value; const days = preset === "custom" ? Number(document.getElementById("customDays").value) : Number(preset); const startDate = document.getElementById("startDate").value || getTodayISO(); const startPage = startFrom.value === "custom" ? clamp(Number(document.getElementById("customStartPage").value || 1), 1, 604) : 1;
if (!title || !days) { showToast("أكمل جميع الحقول المطلوبة"); return; }
createKhatma({ title, type, unit, days, startDate, startPage }); });
document.getElementById("groupForm").addEventListener("submit", (e) => { e.preventDefault(); const groupName = document.getElementById("groupName").value.trim(); const participants = document.getElementById("participants").value;
if (!groupName) { showToast("أدخل اسم المجموعة"); return; } createGroupKhatma(groupName, participants); });
if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker.register("service-worker.js").catch(() => {}); }); }
renderAll();
window.toggleDayCompletion = toggleDayCompletion; window.toggleGroupAssignment = toggleGroupAssignment; '''
manifest_json = r'''{ "name": "ختمتي", "short_name": "ختمتي", "start_url": "./index.html", "display": "standalone", "background_color": "#f5f7f2", "theme_color": "#14532d", "lang": "ar", "dir": "rtl", "description": "تطبيق متابعة الختمات القرآنية على الجوال والحاسب", "icons": [ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" } ] } '''
service_worker = r'''const CACHE_NAME = "khatmati-cache-v1"; const ASSETS = [ "./", "./index.html", "./styles.css", "./app.js", "./manifest.json", "./icon-192.png", "./icon-512.png" ];
self.addEventListener("install", event => { event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))); });
self.addEventListener("fetch", event => { event.respondWith( caches.match(event.request).then(response => response || fetch(event.request)) ); }); '''
readme_md = r'''# ختمتي - Khatmati
تطبيق ويب عربي متجاوب لمتابعة الختمات القرآنية على الجوال والحاسب.
index.html مباشرة في المتصفح.يمكن لاحقًا تغليفه بسهولة عبر:
from PIL import Image, ImageDraw, ImageFont
def make_icon(size, path): img = Image.new("RGB", (size, size), "#14532d") draw = ImageDraw.Draw(img) draw.rounded_rectangle((24, 24, size-24, size-24), radius=round(size0.13), fill="#1d7f3f") draw.ellipse((size0.24, size0.20, size0.76, size0.72), fill="#f4f8f3") draw.rectangle((size0.34, size0.26, size0.66, size0.78), fill="#ffffff") draw.line((size0.40, size0.36, size0.60, size0.36), fill="#14532d", width=max(4, size//64)) draw.line((size0.40, size0.46, size0.60, size0.46), fill="#14532d", width=max(4, size//64)) draw.line((size0.40, size0.56, size0.60, size*0.56), fill="#14532d", width=max(4, size//64)) img.save(path)
(base / "index.html").write_text(index_html, encoding="utf-8") (base / "styles.css").write_text(styles_css, encoding="utf-8") (base / "app.js").write_text(app_js, encoding="utf-8") (base / "manifest.json").write_text(manifest_json, encoding="utf-8") (base / "service-worker.js").write_text(service_worker, encoding="utf-8") (base / "README.md").write_text(readme_md, encoding="utf-8")
make_icon(192, base / "icon-192.png") make_icon(512, base / "icon-512.png")
zip_path = Path("/mnt/data/khatmati_app.zip") with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for file in base.rglob("*"): zf.write(file, file.relative_to(base.parent))
print(f"Created app folder: {base}") print(f"Created zip: {zip_path}") print("Files:") for p in sorted(base.iterdir()): print("-", p.name)
Thought for 1m 1s
تم تحويل الفكرة إلى نسخة تطبيق تعمل على الجوال والحاسب بصيغة تطبيق ويب متجاوب (PWA).
روابط التحميل:
الموجود داخل النسخة:
طريقة التشغيل:
index.html في المتصفح على الكمبيوترمهم:
الخطوة التالية الممكنة: تحويل هذه النسخة إلى APK للأندرويد ونسخة سطح مكتب EXE أو ربطها بقاعدة بيانات ومستخدمين حقيقيين.