صفحه اصلی
<!DOCTYPE html> <html lang="fa" dir="rtl"> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ویکی شخصی من</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #f8f9fa;
--surface: #ffffff;
--sidebar-bg: #f0f2f5;
--border: #a2a9b1;
--border-light: #c8ccd1;
--text: #202122;
--text-muted: #54595d;
--text-light: #72777d;
--accent: #3366cc;
--accent-hover: #2a4b8d;
--accent-visited: #795cb2;
--link: #0645ad;
--link-hover: #0b0080;
--link-visited: #0b0080;
--tab-bg: #eaecf0;
--tab-active: #ffffff;
--toc-bg: #f8f9fa;
--toc-border: #a2a9b1;
--danger: #b32424;
--success: #14866d;
--warning: #e07d10;
--tag-green: #14866d;
--tag-blue: #3366cc;
--tag-orange: #b7570a;
--tag-purple: #795cb2;
--shadow: 0 1px 3px rgba(0,0,0,0.08);
}
body {
font-family: 'Vazirmatn', sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.7;
}
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; color: var(--link-hover); }
/* === Personal Bar (top) === */
.personal-bar {
background: var(--surface);
border-bottom: 1px solid var(--border-light);
padding: 0.25rem 1rem;
font-size: 0.82rem;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 1rem;
color: var(--text-muted);
}
.personal-bar a { color: var(--link); font-size: 0.82rem; }
/* === Main Header === */
.wiki-header {
background: var(--surface);
padding: 0.8rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px solid var(--accent);
}
.wiki-logo {
display: flex;
align-items: center;
gap: 0.75rem;
}
.wiki-logo-icon {
width: 50px;
height: 50px;
background: var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.6rem;
font-weight: 700;
}
.wiki-logo h1 {
font-size: 1.5rem;
font-weight: 400;
color: var(--text);
}
.wiki-logo h1 span {
font-weight: 700;
}
.header-search {
display: flex;
align-items: center;
gap: 0.4rem;
}
.header-search input {
padding: 0.45rem 0.8rem;
border: 1px solid var(--border);
border-radius: 2px;
font-family: inherit;
font-size: 0.9rem;
width: 300px;
direction: rtl;
}
.header-search input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(51,102,204,0.15);
}
.header-search button {
padding: 0.45rem 0.8rem;
background: var(--accent);
color: white;
border: 1px solid var(--accent);
font-family: inherit;
font-size: 0.85rem;
cursor: pointer;
border-radius: 2px;
}
.header-search button:hover { background: var(--accent-hover); }
/* === Body Layout === */
.wiki-body {
display: flex;
max-width: 1400px;
margin: 0 auto;
min-height: calc(100vh - 120px);
}
/* === Sidebar === */
.wiki-sidebar {
width: 220px;
min-width: 220px;
background: var(--surface);
border-left: 1px solid var(--border-light);
padding: 1rem 0;
font-size: 0.88rem;
}
.sidebar-section {
margin-bottom: 0.5rem;
}
.sidebar-section h4 {
padding: 0.4rem 1rem;
font-size: 0.78rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--tab-bg);
border-top: 1px solid var(--border-light);
border-bottom: 1px solid var(--border-light);
}
.sidebar-section ul {
list-style: none;
padding: 0.3rem 0;
}
.sidebar-section li a {
display: block;
padding: 0.3rem 1rem;
color: var(--accent);
transition: background 0.15s;
}
.sidebar-section li a:hover {
background: var(--tab-bg);
text-decoration: none;
}
.sidebar-section li a.active-page {
background: var(--accent);
color: white;
font-weight: 500;
}
.sidebar-search {
padding: 0.5rem 1rem;
}
.sidebar-search input {
width: 100%;
padding: 0.35rem 0.6rem;
border: 1px solid var(--border);
font-family: inherit;
font-size: 0.85rem;
direction: rtl;
}
.sidebar-search input:focus {
outline: none;
border-color: var(--accent);
}
.sidebar-search button {
width: 100%;
margin-top: 0.3rem;
padding: 0.3rem;
background: var(--tab-bg);
border: 1px solid var(--border);
font-family: inherit;
font-size: 0.82rem;
cursor: pointer;
color: var(--text-muted);
}
.sidebar-search button:hover { background: var(--border-light); }
/* === Content Area === */
.wiki-content {
flex: 1;
min-width: 0;
}
/* Tabs */
.content-tabs {
display: flex;
background: var(--tab-bg);
border-bottom: 1px solid var(--border-light);
padding: 0 1rem;
gap: 0;
}
.content-tab {
padding: 0.5rem 1rem;
font-size: 0.88rem;
color: var(--text-muted);
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
background: none;
font-family: inherit;
border-radius: 2px 2px 0 0;
position: relative;
top: 1px;
transition: background 0.15s;
}
.content-tab:hover {
background: var(--surface);
color: var(--text);
}
.content-tab.active {
background: var(--surface);
color: var(--text);
border-color: var(--border-light);
border-bottom-color: var(--surface);
font-weight: 500;
}
/* Article Content */
.article-body {
background: var(--surface);
border: 1px solid var(--border-light);
border-top: none;
padding: 1.5rem 2rem;
min-height: 400px;
}
.article-body h1.article-title {
font-size: 1.8rem;
font-weight: 400;
border-bottom: 1px solid var(--border-light);
padding-bottom: 0.3rem;
margin-bottom: 1rem;
line-height: 1.4;
}
.article-intro {
margin-bottom: 1.5rem;
}
/* Table of Contents */
.toc {
background: var(--toc-bg);
border: 1px solid var(--toc-border);
padding: 0.8rem 1rem;
display: inline-block;
margin: 0 0 1.5rem;
min-width: 200px;
font-size: 0.9rem;
}
.toc-title {
font-weight: 700;
text-align: center;
margin-bottom: 0.3rem;
}
.toc-toggle {
font-size: 0.82rem;
color: var(--link);
cursor: pointer;
margin-right: 0.3rem;
}
.toc ol {
margin: 0;
padding-right: 1.2rem;
list-style-type: decimal;
}
.toc ol ol {
list-style-type: decimal;
}
.toc li {
margin: 0.2rem 0;
}
.toc li a {
color: var(--link);
}
/* Section Headings */
.wiki-heading {
display: flex;
align-items: baseline;
border-bottom: 1px solid var(--border-light);
margin: 1.5rem 0 0.8rem;
padding-bottom: 0.2rem;
}
.wiki-heading h2 {
font-size: 1.4rem;
font-weight: 400;
flex: 1;
}
.wiki-heading .edit-section {
font-size: 0.82rem;
color: var(--text-light);
}
.wiki-heading h3 {
font-size: 1.15rem;
font-weight: 700;
flex: 1;
}
.wiki-heading h3::after {
content: ;
display: none;
}
.article-body p {
margin-bottom: 0.8rem;
}
.article-body ul, .article-body ol {
margin: 0.5rem 0 0.8rem 2rem;
}
.article-body li {
margin-bottom: 0.3rem;
}
/* Category Box */
.catlinks {
background: var(--toc-bg);
border: 1px solid var(--border-light);
padding: 0.5rem 1rem;
margin-top: 2rem;
font-size: 0.85rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.catlinks strong { margin-left: 0.3rem; }
.catlinks a {
background: var(--tab-bg);
padding: 0.15rem 0.5rem;
border-radius: 2px;
font-size: 0.82rem;
}
/* Page Footer */
.page-footer {
background: var(--surface);
border-top: 1px solid var(--border-light);
padding: 1rem 2rem;
font-size: 0.82rem;
color: var(--text-light);
text-align: center;
}
/* === Wiki Text Formatting === */
.wiki-bold { font-weight: 700; }
.wiki-italic { font-style: italic; }
code {
background: var(--toc-bg);
padding: 0.1rem 0.35rem;
border: 1px solid var(--border-light);
font-size: 0.92em;
border-radius: 2px;
font-family: monospace;
}
pre {
background: var(--toc-bg);
border: 1px solid var(--border-light);
padding: 0.8rem 1rem;
overflow-x: auto;
font-size: 0.9rem;
margin: 0.8rem 0;
border-radius: 2px;
}
pre code {
background: none;
border: none;
padding: 0;
}
blockquote {
border-right: 3px solid var(--accent);
padding-right: 1rem;
margin: 0.8rem 0;
color: var(--text-muted);
background: var(--toc-bg);
padding: 0.5rem 1rem;
}
/* Info Box */
.infobox {
float: left;
clear: right;
margin: 0 0 1rem 1.5rem;
border: 1px solid var(--border-light);
border-collapse: collapse;
background: var(--surface);
font-size: 0.88rem;
width: 280px;
}
.infobox-header {
background: var(--accent);
color: white;
padding: 0.5rem 0.8rem;
text-align: center;
font-weight: 700;
font-size: 1rem;
}
.infobox td, .infobox th {
padding: 0.3rem 0.6rem;
border: 1px solid var(--border-light);
vertical-align: top;
}
.infobox th {
background: var(--toc-bg);
text-align: right;
font-weight: 500;
width: 35%;
}
/* Alert Boxes */
.ambox {
border: 1px solid var(--border-light);
background: var(--toc-bg);
padding: 0.6rem 1rem;
margin: 0.8rem 0;
font-size: 0.88rem;
display: flex;
align-items: center;
gap: 0.6rem;
}
.ambox-icon { font-size: 1.3rem; }
/* Editor (Modal) */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 200;
align-items: center;
justify-content: center;
padding: 1rem;
}
.modal-overlay.show { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
width: 100%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
border-radius: 2px;
}
.modal-header {
background: var(--tab-bg);
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-header h3 { font-size: 1rem; font-weight: 500; }
.modal-body { padding: 1.2rem; }
.form-group { margin-bottom: 1rem; }
.form-group label {
display: block;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.3rem;
color: var(--text-muted);
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.5rem 0.7rem;
border: 1px solid var(--border);
font-family: inherit;
font-size: 0.95rem;
direction: rtl;
border-radius: 2px;
}
.form-group textarea {
min-height: 300px;
resize: vertical;
font-family: monospace;
font-size: 0.9rem;
line-height: 1.8;
direction: ltr;
text-align: left;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--accent);
}
.modal-actions {
display: flex;
gap: 0.5rem;
padding: 0.6rem 1rem;
background: var(--tab-bg);
border-top: 1px solid var(--border-light);
}
.modal-actions button {
padding: 0.4rem 1rem;
font-family: inherit;
font-size: 0.85rem;
cursor: pointer;
border-radius: 2px;
}
.btn-save {
background: var(--accent);
color: white;
border: 1px solid var(--accent);
}
.btn-save:hover { background: var(--accent-hover); }
.btn-cancel {
background: var(--tab-bg);
color: var(--text);
border: 1px solid var(--border);
}
.btn-cancel:hover { background: var(--border-light); }
.btn-delete {
background: var(--danger);
color: white;
border: 1px solid var(--danger);
margin-right: auto;
}
.btn-delete:hover { background: #9b1b1b; }
/* Footer nav */
.footer-nav {
background: var(--surface);
border-top: 1px solid var(--border-light);
padding: 0.5rem 1rem;
display: flex;
justify-content: center;
gap: 1.5rem;
font-size: 0.82rem;
color: var(--text-light);
}
.footer-nav a { color: var(--link); }
/* Special pages list */
.special-list {
list-style: none;
padding: 0;
}
.special-list li {
padding: 0.4rem 0;
border-bottom: 1px dotted var(--border-light);
}
.special-list li:last-child { border-bottom: none; }
.special-list a {
font-weight: 500;
}
.special-list .desc {
color: var(--text-muted);
font-size: 0.88rem;
margin-right: 0.5rem;
}
/* Responsive */
@media (max-width: 900px) {
.wiki-sidebar { display: none; }
.header-search input { width: 180px; }
.article-body { padding: 1rem; }
.infobox { float: none; width: 100%; margin: 0 0 1rem; }
}
</style>
</head> <body>
<header class="wiki-header">
ویکی شخصی من
<input type="text" id="globalSearch" placeholder="جستجو..." oninput="searchPages()">
<button onclick="searchPages()">جستجو</button>
</header>
<nav class="wiki-sidebar" id="sidebar">
</nav>
<button class="content-tab active" id="tab-read" onclick="switchTab('read')">خواندن</button>
<button class="content-tab" id="tab-edit" onclick="switchTab('edit')">ویرایش</button>
<input type="file" id="importInput" accept=".json" style="display:none;" onchange="handleImport(event)">
<script>
// ===== Data Layer =====
let pages = JSON.parse(localStorage.getItem('mediawiki_pages') || '[]');
let currentPageId = null;
let editingPageId = null;
function save() {
localStorage.setItem('mediawiki_pages', JSON.stringify(pages));
}
function genId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
function getPageBySlug(slug) {
return pages.find(p => p.slug === slug);
}
function slugify(text) {
return text.trim().replace(/\s+/g, '-').replace(/[^\w\-\u0600-\u06FF]/g, ).toLowerCase();
}
// ===== Wiki Markup Parser =====
function parseWikiMarkup(text) {
let html = text;
// Escape HTML first
html = html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
// Headings: == Title == →
, === Title === →
, etc. html = html.replace(/^======\s*(.+?)\s*======/gm, '
$1
'); html = html.replace(/^=====\s*(.+?)\s*=====/gm, '
$1
'); html = html.replace(/^====\s*(.+?)\s*====/gm, '
$1
'); html = html.replace(/^===\s*(.+?)\s*===/gm, '
$1
'); html = html.replace(/^==\s*(.+?)\s*==/gm, '
$1
// Bold & Italic
html = html.replace(/'{5}(.+?)'{5}/g, '$1');
html = html.replace(/'{3}(.+?)'{3}/g, '$1');
html = html.replace(/'{2}(.+?)'{2}/g, '$1');
// Internal links: Page Name or display text html = html.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '<a href="#" onclick="navigateToPage(\'$1\'); return false;" class="wiki-link">$2</a>'); html = html.replace(/\[\[([^\]]+)\]\]/g, '<a href="#" onclick="navigateToPage(\'$1\'); return false;" class="wiki-link">$1</a>');
// External links: text html = html.replace(/\[(https?:\/\/[^\s]+)\s+([^\]]+)\]/g, '<a href="$1" target="_blank">$2</a>'); // Bare external: [۱] html = html.replace(/\[(https?:\/\/[^\s]+)\]/g, '<a href="$1" target="_blank">$1</a>');
// Templates: الگو:یادآوری or الگو:Tiphtml = html.replace(/\{\{یادآوری\|(.+?)\}\}/g, '
// Code blocks: ... or indented lines
html = html.replace(/([\s\S]*?)<\/code>/g, '<code>$1</code>
');
// Blockquote: lines starting with :
html = html.replace(/^:\s*(.+)$/gm, '$1
');
// Horizontal rule: ----
html = html.replace(/^----+$/gm, '
');
// Unordered lists
html = html.replace(/^\* (.+)$/gm, '$1 ');
// Ordered lists
html = html.replace(/^# (.+)$/gm, '$1 ');
// Wrap consecutive in
html = html.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, (match) => {
if (match.includes('list-style:decimal')) return '' + match + '
';
return '' + match + '
';
});
// Line breaks: double newline = paragraph
html = html.replace(/\n\n+/g, '');
html = html.replace(/\n/g, '
');
// Wrap in paragraphs
html = '' + html + '
';
// Remove empty paragraphs
html = html.replace(/<\/p>/g, );
html = html.replace(/
\s*<\/p>/g, );
return html;
}
// ===== Rendering =====
function showMainPage() {
currentPageId = null;
updateSidebarActive('صفحه اصلی');
const defaultContent = pages.length === 0
? getWelcomeContent()
: getRecentChangesContent();
document.getElementById('readView').innerHTML = defaultContent;
document.getElementById('tab-read').click();
document.getElementById('footerDate').textContent = formatDate(new Date().toISOString());
window.scrollTo(0, 0);
}
function getWelcomeContent() {
return `
صفحه اصلی
به ویکی شخصی خود خوش آمدید! اینجا میتوانید هر چیزی که یاد میگیرید را ذخیره کنید.
نحوه استفاده
برای شروع کار با این ویکی، از امکانات زیر استفاده کنید:
- ایجاد صفحه جدید: از لینک «صفحه جدید» در بالای صفحه استفاده کنید.
- ویرایش صفحه: روی تب «ویرایش» کلیک کنید.
- پیوند بین صفحات: از
نام صفحه استفاده کنید.
- قالببندی: از
== عنوان == برای تیتر و متن برای بولد استفاده کنید.
شروع کنید
اکنون <a href="#" onclick="openCreateModal(); return false;">اولین صفحه خود را بسازید</a> و شروع به یادداشت کنید!
راهنمای Wiki markup
نمونههای قالببندی
عنوان بخش == عنوان == یا === زیرعنوان ===
متن بولد متن بولد
متن ایتالیک متن ایتالیک
لیست * مورد اول (ترتیبی: # مورد اول)
پیوند داخلی نام صفحه یا متن نمایشی
پیوند خارجی متن لینک
یادآوری الگو:یادآوری
کد <code>کد</code>
خط جداکننده ----
`;
}
function getRecentChangesContent() {
const sorted = [...pages].sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
const recent = sorted.slice(0, 10);
let html = `
صفحه اصلی
به ویکی شخصی خود خوش آمدید. <a href="#" onclick="openCreateModal(); return false;">صفحه جدید ایجاد کنید</a>.
تغییرات اخیر
آخرین صفحات ویرایش شده:
`;
recent.forEach(p => {
const cats = p.categories.length > 0 ? p.categories.join(', ') : '—';
html += `
`;
});
html += `
صفحه
رده
آخرین ویرایش
<a href="#" onclick="event.preventDefault();navigateToPage('${p.title}')">${escapeHtml(p.title)}</a>
${escapeHtml(cats)}
${formatDate(p.updatedAt)}
`;
// Stats
html += `
آمار ویکی
- تعداد کل صفحات: ${pages.length}
- تعداد ردهها: ${new Set(pages.flatMap(p => p.categories)).size}
- بزرگترین صفحه: ${pages.length > 0 ? pages.reduce((a, b) => a.content.length > b.content.length ? a : b).title : '—'}
`;
return html;
}
function showAllPages() {
currentPageId = null;
updateSidebarActive('همه صفحات');
const sorted = [...pages].sort((a, b) => a.title.localeCompare(b.title, 'fa'));
let html = `همه صفحات
`;
if (sorted.length === 0) {
html += `
هنوز هیچ صفحهای وجود ندارد. <a href="#" onclick="openCreateModal(); return false;">اولین صفحه را بسازید</a>.
`;
} else {
html += `تعداد کل صفحات: ${sorted.length}
`;
html += ``;
sorted.forEach(p => {
const catText = p.categories.length > 0 ? ` — ${p.categories.join(', ')}` : ;
html += `- <a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a> ${catText} (${formatDate(p.updatedAt)})
`;
});
html += `
`;
}
document.getElementById('readView').innerHTML = html;
document.getElementById('tab-read').click();
document.getElementById('footerDate').textContent = formatDate(new Date().toISOString());
window.scrollTo(0, 0);
}
function navigateToPage(title) {
const page = pages.find(p => p.title === title || p.slug === slugify(title));
if (page) {
showPage(page.id);
} else {
// Page doesn't exist — offer to create
if (confirm(`صفحه «${title}}» وجود ندارد. آیا میخواهید آن را بسازید؟`)) {
editingPageId = null;
document.getElementById('modalTitle').textContent = `ایجاد صفحه: ${title}`;
document.getElementById('pageTitle').value = title;
document.getElementById('pageCategory').value = ;
document.getElementById('pageContent').value = ;
document.getElementById('modalDeleteBtn').style.display = 'none';
document.getElementById('editorModal').classList.add('show');
}
}
}
function showPage(id) {
const page = pages.find(p => p.id === id);
if (!page) return showMainPage();
currentPageId = id;
updateSidebarActive(page.title);
const parsedContent = parseWikiMarkup(page.content);
// Build TOC from headings
const headingMatches = page.content.match(/^==+\s*(.+?)\s*==+$/gm) || [];
let tocHtml = ;
if (headingMatches.length >= 2) {
tocHtml = `فهرست مطالب [隐蔽]`;
headingMatches.forEach((h, i) => {
const depth = (h.match(/^=+/) || ['=='])[0].length - 2;
const text = h.replace(/^=+\s*/, ).replace(/\s*=+$/, );
const indent = depth > 0 ? `style="margin-right:${depth * 1.5}rem;"` : ;
tocHtml += `- <a href="#section-${i}">${text}</a>
`;
});
tocHtml += `
`;
}
// Build categories
let catHtml = ;
if (page.categories.length > 0) {
catHtml = `ردهها: ${page.categories.map(c => `<a href="#" onclick="filterByCategory('${escapeHtml(c)}'); return false;">${escapeHtml(c)}</a>`).join(' ')}`;
}
// Infobox
const wordCount = page.content.split(/\s+/).length;
const charCount = page.content.length;
const infobox = `
${escapeHtml(page.title)}
${page.categories.length > 0 ? `` : }
تاریخ ایجاد ${formatDate(page.createdAt)}
آخرین ویرایش ${formatDate(page.updatedAt)}
تعداد کلمات ${wordCount.toLocaleString('fa-IR')}
تعداد حروف ${charCount.toLocaleString('fa-IR')} ردهها ${page.categories.join('، ')}
`;
// Assemble
let fullHtml = `${escapeHtml(page.title)}
`;
fullHtml += infobox;
fullHtml += tocHtml;
fullHtml += parsedContent;
fullHtml += ``;
fullHtml += catHtml;
// See also — links to other pages
const otherPages = pages.filter(p => p.id !== page.id).slice(0, 5);
if (otherPages.length > 0) {
fullHtml += `همچنین ببینید
`;
fullHtml += ``;
otherPages.forEach(p => {
fullHtml += `- <a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a>
`;
});
fullHtml += `
`;
}
document.getElementById('readView').innerHTML = fullHtml;
document.getElementById('tab-read').click();
document.getElementById('footerDate').textContent = formatDate(page.updatedAt);
window.scrollTo(0, 0);
updateSidebar();
}
function renderEditView(id) {
const page = id ? pages.find(p => p.id === id) : null;
let html = `${page ? 'ویرایش: ' + escapeHtml(page.title) : 'ایجاد صفحه جدید'}
`;
html += `
راهنمای سریع Wiki markup:
== عنوان == · بولد · ایتالیک · پیوند · الگو:یادآوری · * لیست · <code>kod</code>
<label>عنوان صفحه</label>
<input type="text" id="editTitle" value="${page ? escapeHtml(page.title) : }" placeholder="عنوان صفحه">
<label>ردهها (با کاما جدا کنید)</label>
<input type="text" id="editCategories" value="${page ? page.categories.join(', ') : }" placeholder="برنامهنویسی, یادداشت">
<label>محتوا</label>
<textarea id="editContent" placeholder="محتوای صفحه را اینجا بنویسید...">${page ? page.content : }</textarea>
<button class="btn-save" onclick="saveFromEditor(${id ? `'${id}'` : 'null'})">ذخیره صفحه</button>
<button class="btn-cancel" onclick="cancelEdit()">لغو</button>
${page ? `<button class="btn-delete" onclick="deletePage('${id}')">حذف صفحه</button>` : }
`;
document.getElementById('editView').innerHTML = html;
}
function switchTab(tab) {
if (tab === 'read') {
document.getElementById('tab-read').classList.add('active');
document.getElementById('tab-edit').classList.remove('active');
document.getElementById('readView').style.display = ;
document.getElementById('editView').style.display = 'none';
} else {
document.getElementById('tab-edit').classList.add('active');
document.getElementById('tab-read').classList.remove('active');
document.getElementById('readView').style.display = 'none';
document.getElementById('editView').style.display = ;
renderEditView(currentPageId);
}
}
function cancelEdit() {
if (currentPageId) {
switchTab('read');
} else {
showMainPage();
}
}
function saveFromEditor(id) {
const title = document.getElementById('editTitle').value.trim();
const cats = document.getElementById('editCategories').value.split(',').map(c => c.trim()).filter(Boolean);
const content = document.getElementById('editContent').value;
if (!title) { document.getElementById('editTitle').focus(); return; }
if (!content) { document.getElementById('editContent').focus(); return; }
if (id) {
const page = pages.find(p => p.id === id);
if (page) {
page.title = title;
page.slug = slugify(title);
page.categories = cats;
page.content = content;
page.updatedAt = new Date().toISOString();
}
} else {
const newPage = {
id: genId(),
title,
slug: slugify(title),
categories: cats,
content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
pages.push(newPage);
currentPageId = newPage.id;
}
save();
showPage(currentPageId);
updateSidebar();
}
// ===== Modal helpers =====
function openCreateModal() {
editingPageId = null;
document.getElementById('modalTitle').textContent = 'ایجاد صفحه جدید';
document.getElementById('pageTitle').value = ;
document.getElementById('pageCategory').value = ;
document.getElementById('pageContent').value = ;
document.getElementById('modalDeleteBtn').style.display = 'none';
document.getElementById('editorModal').classList.add('show');
document.getElementById('pageTitle').focus();
}
function savePage() {
const title = document.getElementById('pageTitle').value.trim();
const cats = document.getElementById('pageCategory').value.split(',').map(c => c.trim()).filter(Boolean);
const content = document.getElementById('pageContent').value;
if (!title) { document.getElementById('pageTitle').focus(); return; }
if (!content) { document.getElementById('pageContent').focus(); return; }
const newPage = {
id: genId(),
title,
slug: slugify(title),
categories: cats,
content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
pages.push(newPage);
save();
closeModal('editorModal');
currentPageId = newPage.id;
showPage(newPage.id);
updateSidebar();
}
function deletePage(id) {
if (!confirm('آیا مطمئن هستید که میخواهید این صفحه را حذف کنید؟')) return;
pages = pages.filter(p => p.id !== id);
save();
currentPageId = null;
showMainPage();
updateSidebar();
}
function deleteCurrentPage() {
if (currentPageId) deletePage(currentPageId);
}
function closeModal(id) {
document.getElementById(id).classList.remove('show');
}
// ===== Sidebar =====
function updateSidebar() {
const navPages = document.getElementById('navPages');
const sorted = [...pages].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
const recent = sorted.slice(0, 8);
let html = ` <a href="#" class="${!currentPageId ? 'active-page' : }" onclick="showMainPage(); return false;">صفحه اصلی</a> `;
recent.forEach(p => {
html += `<a href="#" class="${currentPageId === p.id ? 'active-page' : }" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a> `;
});
navPages.innerHTML = html;
// Categories
const allCats = [...new Set(pages.flatMap(p => p.categories))].sort();
const catList = document.getElementById('categoryList');
catList.innerHTML = allCats.map(c =>
`<a href="#" onclick="filterByCategory('${escapeHtml(c)}'); return false;">${escapeHtml(c)}</a> `
).join();
}
function updateSidebarActive(title) {
document.querySelectorAll('.sidebar-section li a').forEach(a => {
a.classList.remove('active-page');
if (a.textContent.trim() === title) a.classList.add('active-page');
});
}
function filterByCategory(cat) {
const matching = pages.filter(p => p.categories.includes(cat));
let html = `رده: ${escapeHtml(cat)}
`;
html += `تعداد صفحات در این رده: ${matching.length}
`;
if (matching.length > 0) {
html += ``;
matching.forEach(p => {
html += `- <a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a> (${formatDate(p.updatedAt)})
`;
});
html += `
`;
}
document.getElementById('readView').innerHTML = html;
document.getElementById('tab-read').click();
window.scrollTo(0, 0);
}
// ===== Search =====
function searchPages() {
const query = document.getElementById('globalSearch').value.trim().toLowerCase() ||
document.getElementById('sidebarSearch').value.trim().toLowerCase();
if (!query) {
if (currentPageId) return showPage(currentPageId);
return showMainPage();
}
const results = pages.filter(p =>
p.title.toLowerCase().includes(query) ||
p.content.toLowerCase().includes(query) ||
p.categories.some(c => c.toLowerCase().includes(query))
);
let html = `نتایج جستجو: «${escapeHtml(query)}»
`;
html += `${results.length} نتیجه یافت شد.
`;
if (results.length > 0) {
html += ``;
results.forEach(p => {
// Show context snippet
const idx = p.content.toLowerCase().indexOf(query);
let snippet = ;
if (idx !== -1) {
const start = Math.max(0, idx - 40);
const end = Math.min(p.content.length, idx + query.length + 60);
snippet = (start > 0 ? '...' : ) + p.content.substring(start, end) + (end < p.content.length ? '...' : );
}
html += `- <a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a>`;
if (snippet) html += `
${escapeHtml(snippet)}`;
html += ` `;
});
html += `
`;
}
document.getElementById('readView').innerHTML = html;
document.getElementById('tab-read').click();
window.scrollTo(0, 0);
}
// ===== Import/Export =====
function exportData() {
const data = JSON.stringify(pages, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `wiki-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
}
function importData() {
document.getElementById('importInput').click();
}
function handleImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!Array.isArray(data)) throw new Error('Invalid format');
if (confirm(`آیا میخواهید ${data.length} صفحه را وارد کنید؟ (صفحات فعلی حفظ میشوند)`)) {
data.forEach(p => {
if (p.title && p.content) {
pages.push({
id: p.id || genId(),
title: p.title,
slug: p.slug || slugify(p.title),
categories: p.categories || [],
content: p.content,
createdAt: p.createdAt || new Date().toISOString(),
updatedAt: p.updatedAt || new Date().toISOString(),
});
}
});
save();
showMainPage();
updateSidebar();
alert(`${data.length} صفحه با موفقیت وارد شد.`);
}
} catch (err) {
alert('خطا در خواندن فایل. لطفاً فایل JSON معتبر وارد کنید.');
}
};
reader.readAsText(file);
event.target.value = ;
}
// ===== Helpers =====
function formatDate(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('fa-IR', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
// ===== Keyboard Shortcuts =====
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.show').forEach(m => m.classList.remove('show'));
}
if (e.key === 'n' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
openCreateModal();
}
});
// Close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(o => {
o.addEventListener('click', (e) => { if (e.target === o) o.classList.remove('show'); });
});
// ===== Init =====
updateSidebar();
showMainPage();
</script>
</body>
</html>