|
|
| خط ۱: |
خط ۱: |
| <!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>
| |
|
| |
| <!-- Personal Toolbar -->
| |
| <div class="personal-bar">
| |
| <span>👤 کاربر مهمان</span>
| |
| <a href="#" onclick="openCreateModal(); return false;">صفحه جدید</a>
| |
| <a href="#" onclick="showAllPages(); return false;">همه صفحات</a>
| |
| <a href="#" onclick="exportData(); return false;">خروجی JSON</a>
| |
| <a href="#" onclick="importData(); return false;">ورودی JSON</a>
| |
| </div>
| |
|
| |
| <!-- Header -->
| |
| <header class="wiki-header">
| |
| <div class="wiki-logo">
| |
| <div class="wiki-logo-icon">W</div>
| |
| <h1><span>ویکی</span> شخصی من</h1>
| |
| </div>
| |
| <div class="header-search">
| |
| <input type="text" id="globalSearch" placeholder="جستجو..." oninput="searchPages()">
| |
| <button onclick="searchPages()">جستجو</button>
| |
| </div>
| |
| </header>
| |
|
| |
| <!-- Body -->
| |
| <div class="wiki-body">
| |
| <!-- Sidebar -->
| |
| <nav class="wiki-sidebar" id="sidebar">
| |
| <div class="sidebar-section">
| |
| <h4>ابزارها</h4>
| |
| <div class="sidebar-search">
| |
| <input type="text" id="sidebarSearch" placeholder="جستجو در صفحات..." oninput="searchPages()">
| |
| <button onclick="searchPages()">برو</button>
| |
| </div>
| |
| </div>
| |
| <div class="sidebar-section">
| |
| <h4>ناوبری</h4>
| |
| <ul id="navPages">
| |
| <li><a href="#" class="active-page" onclick="showMainPage(); return false;">صفحه اصلی</a></li>
| |
| </ul>
| |
| </div>
| |
| <div class="sidebar-section">
| |
| <h4>صفحات ویژه</h4>
| |
| <ul>
| |
| <li><a href="#" onclick="showAllPages(); return false;">همه صفحات</a></li>
| |
| <li><a href="#" onclick="openCreateModal(); return false;">ایجاد صفحه جدید</a></li>
| |
| </ul>
| |
| </div>
| |
| <div class="sidebar-section" id="categorySidebar">
| |
| <h4>ردهها</h4>
| |
| <ul id="categoryList"></ul>
| |
| </div>
| |
| </nav>
| |
|
| |
| <!-- Content -->
| |
| <div class="wiki-content">
| |
| <div class="content-tabs" id="contentTabs">
| |
| <button class="content-tab active" id="tab-read" onclick="switchTab('read')"><strong>خواندن</strong></button>
| |
| <button class="content-tab" id="tab-edit" onclick="switchTab('edit')"><strong>ویرایش</strong></button>
| |
| </div>
| |
|
| |
| <!-- Read View -->
| |
| <div class="article-body" id="readView"></div>
| |
|
| |
| <!-- Edit View (hidden) -->
| |
| <div class="article-body" id="editView" style="display:none;"></div>
| |
|
| |
| <!-- Footer -->
| |
| <div class="page-footer">
| |
| صفحه آخرین بار در <span id="footerDate"></span> ویرایش شده است.
| |
| </div>
| |
| </div>
| |
| </div>
| |
|
| |
| <!-- Bottom nav -->
| |
| <div class="footer-nav">
| |
| <a href="#" onclick="showMainPage(); return false;">صفحه اصلی</a>
| |
| <span>•</span>
| |
| <a href="#" onclick="showAllPages(); return false;">فهرست صفحات</a>
| |
| <span>•</span>
| |
| <a href="#" onclick="openCreateModal(); return false;">صفحه جدید</a>
| |
| <span>•</span>
| |
| <span>ذخیره شده در مرورگر شما ❤️</span>
| |
| </div>
| |
|
| |
| <!-- Create/Edit Modal -->
| |
| <div class="modal-overlay" id="editorModal">
| |
| <div class="modal">
| |
| <div class="modal-header">
| |
| <h3 id="modalTitle">ایجاد صفحه جدید</h3>
| |
| <button class="btn-cancel" onclick="closeModal('editorModal')" style="border:none;background:none;font-size:1.2rem;cursor:pointer;">✕</button>
| |
| </div>
| |
| <div class="modal-body">
| |
| <div class="form-group">
| |
| <label>نام صفحه (عنوان)</label>
| |
| <input type="text" id="pageTitle" placeholder="مثال: React Hooks">
| |
| </div>
| |
| <div class="form-group">
| |
| <label>رده (دستهبندی)</label>
| |
| <input type="text" id="pageCategory" placeholder="مثال: برنامهنویسی، طراحی، یادداشت">
| |
| </div>
| |
| <div class="form-group">
| |
| <label>محتوای ویکی (Wiki markup ساده)</label>
| |
| <textarea id="pageContent" placeholder="== عنوان بخش == متن مقاله اینجا... === زیرعنوان === * مورد اول * مورد دوم {{یادآوری|متن مهم}} [[پیوند به صفحه دیگر]] ---"></textarea>
| |
| </div>
| |
| </div>
| |
| <div class="modal-actions">
| |
| <button class="btn-save" onclick="savePage()">ذخیره صفحه</button>
| |
| <button class="btn-cancel" onclick="closeModal('editorModal')">لغو</button>
| |
| <button class="btn-delete" id="modalDeleteBtn" style="display:none;" onclick="deleteCurrentPage()">حذف صفحه</button>
| |
| </div>
| |
| </div>
| |
| </div>
| |
|
| |
| <!-- Hidden file input for import -->
| |
| <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 == → <h2>, === Title === → <h3>, etc.
| |
| html = html.replace(/^======\s*(.+?)\s*======/gm, '<h5>$1</h5>');
| |
| html = html.replace(/^=====\s*(.+?)\s*=====/gm, '<h4>$1</h4>');
| |
| html = html.replace(/^====\s*(.+?)\s*====/gm, '<h4>$1</h4>');
| |
| html = html.replace(/^===\s*(.+?)\s*===/gm, '<h3>$1</h3>');
| |
| html = html.replace(/^==\s*(.+?)\s*==/gm, '</div><div class="wiki-heading"><h2>$1</h2></div><div class="wiki-para">');
| |
|
| |
| // Bold & Italic
| |
| html = html.replace(/'{5}(.+?)'{5}/g, '<b><i>$1</i></b>');
| |
| html = html.replace(/'{3}(.+?)'{3}/g, '<b>$1</b>');
| |
| html = html.replace(/'{2}(.+?)'{2}/g, '<i>$1</i>');
| |
|
| |
| // Internal links: [[Page Name]] or [[Page Name|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: [http://... text]
| |
| html = html.replace(/\[(https?:\/\/[^\s]+)\s+([^\]]+)\]/g, '<a href="$1" target="_blank">$2</a>');
| |
| // Bare external: [http://...]
| |
| html = html.replace(/\[(https?:\/\/[^\s]+)\]/g, '<a href="$1" target="_blank">$1</a>');
| |
|
| |
| // Templates: {{یادآوری|text}} or {{tip|text}}
| |
| html = html.replace(/\{\{یادآوری\|(.+?)\}\}/g, '<div class="ambox"><span class="ambox-icon">💡</span><span>$1</span></div>');
| |
| html = html.replace(/\{\{tip\|(.+?)\}\}/g, '<div class="ambox"><span class="ambox-icon">✅</span><span>$1</span></div>');
| |
| html = html.replace(/\{\{هشدار\|(.+?)\}\}/g, '<div class="ambox" style="border-color:var(--danger);"><span class="ambox-icon">⚠️</span><span>$1</span></div>');
| |
| html = html.replace(/\{\{note\|(.+?)\}\}/g, '<div class="ambox"><span class="ambox-icon">📝</span><span>$1</span></div>');
| |
|
| |
| // Code blocks: <code>...</code> or indented lines
| |
| html = html.replace(/<code>([\s\S]*?)<\/code>/g, '<pre><code>$1</code></pre>');
| |
|
| |
| // Blockquote: lines starting with :
| |
| html = html.replace(/^:\s*(.+)$/gm, '<blockquote>$1</blockquote>');
| |
|
| |
| // Horizontal rule: ----
| |
| html = html.replace(/^----+$/gm, '<hr style="border:none;border-top:1px solid var(--border-light);margin:1rem 0;">');
| |
|
| |
| // Unordered lists
| |
| html = html.replace(/^\* (.+)$/gm, '<li style="list-style:disc;margin-right:1.5rem;">$1</li>');
| |
|
| |
| // Ordered lists
| |
| html = html.replace(/^# (.+)$/gm, '<li style="list-style:decimal;margin-right:1.5rem;">$1</li>');
| |
|
| |
| // Wrap consecutive <li> in <ul>
| |
| html = html.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, (match) => {
| |
| if (match.includes('list-style:decimal')) return '<ol style="margin:0.5rem 0;">' + match + '</ol>';
| |
| return '<ul style="margin:0.5rem 0;">' + match + '</ul>';
| |
| });
| |
|
| |
| // Line breaks: double newline = paragraph
| |
| html = html.replace(/\n\n+/g, '</p><p>');
| |
| html = html.replace(/\n/g, '<br>');
| |
|
| |
| // Wrap in paragraphs
| |
| html = '<div class="wiki-para"><p>' + html + '</p></div>';
| |
|
| |
| // Remove empty paragraphs
| |
| html = html.replace(/<p><\/p>/g, '');
| |
| html = html.replace(/<p>\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 `
| |
| <h1 class="article-title">صفحه اصلی</h1>
| |
| <div class="ambox">
| |
| <span class="ambox-icon">📖</span>
| |
| <span>به ویکی شخصی خود خوش آمدید! اینجا میتوانید هر چیزی که یاد میگیرید را ذخیره کنید.</span>
| |
| </div>
| |
| <h2 style="font-weight:400;font-size:1.4rem;margin:1.5rem 0 0.5rem;border-bottom:1px solid var(--border-light);padding-bottom:0.2rem;">نحوه استفاده</h2>
| |
| <p>برای شروع کار با این ویکی، از امکانات زیر استفاده کنید:</p>
| |
| <ul style="margin:0.5rem 0 1rem 2rem;">
| |
| <li><b>ایجاد صفحه جدید:</b> از لینک «صفحه جدید» در بالای صفحه استفاده کنید.</li>
| |
| <li><b>ویرایش صفحه:</b> روی تب «ویرایش» کلیک کنید.</li>
| |
| <li><b>پیوند بین صفحات:</b> از <code>[[نام صفحه]]</code> استفاده کنید.</li>
| |
| <li><b>قالببندی:</b> از <code>== عنوان ==</code> برای تیتر و <code>'''متن'''</code> برای بولد استفاده کنید.</li>
| |
| </ul>
| |
| <h2 style="font-weight:400;font-size:1.4rem;margin:1.5rem 0 0.5rem;border-bottom:1px solid var(--border-light);padding-bottom:0.2rem;">شروع کنید</h2>
| |
| <p>اکنون <a href="#" onclick="openCreateModal(); return false;"><b>اولین صفحه خود را بسازید</b></a> و شروع به یادداشت کنید!</p>
| |
| <h2 style="font-weight:400;font-size:1.4rem;margin:1.5rem 0 0.5rem;border-bottom:1px solid var(--border-light);padding-bottom:0.2rem;">راهنمای Wiki markup</h2>
| |
| <table class="infobox" style="width:100%;float:none;">
| |
| <tr><td class="infobox-header" colspan="2">نمونههای قالببندی</td></tr>
| |
| <tr><th>عنوان بخش</th><td><code>== عنوان ==</code> یا <code>=== زیرعنوان ===</code></td></tr>
| |
| <tr><th>متن بولد</th><td><code>'''متن بولد'''</code></td></tr>
| |
| <tr><th>متن ایتالیک</th><td><code>''متن ایتالیک''</code></td></tr>
| |
| <tr><th>لیست</th><td><code>* مورد اول</code> (ترتیبی: <code># مورد اول</code>)</td></tr>
| |
| <tr><th>پیوند داخلی</th><td><code>[[نام صفحه]]</code> یا <code>[[نام|متن نمایشی]]</code></td></tr>
| |
| <tr><th>پیوند خارجی</th><td><code>[https://example.com متن لینک]</code></td></tr>
| |
| <tr><th>یادآوری</th><td><code>{{یادآوری|متن مهم}}</code></td></tr>
| |
| <tr><th>کد</th><td><code><code>کد</code></code></td></tr>
| |
| <tr><th>خط جداکننده</th><td><code>----</code></td></tr>
| |
| </table>
| |
| <div style="clear:both;"></div>`;
| |
| }
| |
|
| |
| function getRecentChangesContent() {
| |
| const sorted = [...pages].sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
| |
| const recent = sorted.slice(0, 10);
| |
|
| |
| let html = `
| |
| <h1 class="article-title">صفحه اصلی</h1>
| |
| <div class="ambox">
| |
| <span class="ambox-icon">🏠</span>
| |
| <span>به ویکی شخصی خود خوش آمدید. <a href="#" onclick="openCreateModal(); return false;">صفحه جدید ایجاد کنید</a>.</span>
| |
| </div>
| |
| <h2 style="font-weight:400;font-size:1.4rem;margin:1.5rem 0 0.5rem;border-bottom:1px solid var(--border-light);padding-bottom:0.2rem;">تغییرات اخیر</h2>
| |
| <p>آخرین صفحات ویرایش شده:</p>
| |
| <table style="width:100%;border-collapse:collapse;margin:0.5rem 0;">
| |
| <tr style="background:var(--tab-bg);">
| |
| <th style="padding:0.4rem 0.6rem;border:1px solid var(--border-light);text-align:right;">صفحه</th>
| |
| <th style="padding:0.4rem 0.6rem;border:1px solid var(--border-light);text-align:right;">رده</th>
| |
| <th style="padding:0.4rem 0.6rem;border:1px solid var(--border-light);text-align:right;">آخرین ویرایش</th>
| |
| </tr>`;
| |
|
| |
| recent.forEach(p => {
| |
| const cats = p.categories.length > 0 ? p.categories.join(', ') : '—';
| |
| html += `
| |
| <tr style="cursor:pointer;" onclick="navigateToPage('${p.title}')">
| |
| <td style="padding:0.4rem 0.6rem;border:1px solid var(--border-light);"><a href="#" onclick="event.preventDefault();navigateToPage('${p.title}')">${escapeHtml(p.title)}</a></td>
| |
| <td style="padding:0.4rem 0.6rem;border:1px solid var(--border-light);color:var(--text-muted);">${escapeHtml(cats)}</td>
| |
| <td style="padding:0.4rem 0.6rem;border:1px solid var(--border-light);color:var(--text-light);">${formatDate(p.updatedAt)}</td>
| |
| </tr>`;
| |
| });
| |
|
| |
| html += `</table>`;
| |
|
| |
| // Stats
| |
| html += `
| |
| <h2 style="font-weight:400;font-size:1.4rem;margin:1.5rem 0 0.5rem;border-bottom:1px solid var(--border-light);padding-bottom:0.2rem;">آمار ویکی</h2>
| |
| <ul style="margin:0.5rem 0 1rem 2rem;">
| |
| <li>تعداد کل صفحات: <b>${pages.length}</b></li>
| |
| <li>تعداد ردهها: <b>${new Set(pages.flatMap(p => p.categories)).size}</b></li>
| |
| <li>بزرگترین صفحه: <b>${pages.length > 0 ? pages.reduce((a, b) => a.content.length > b.content.length ? a : b).title : '—'}</b></li>
| |
| </ul>`;
| |
|
| |
| return html;
| |
| }
| |
|
| |
| function showAllPages() {
| |
| currentPageId = null;
| |
| updateSidebarActive('همه صفحات');
| |
|
| |
| const sorted = [...pages].sort((a, b) => a.title.localeCompare(b.title, 'fa'));
| |
|
| |
| let html = `<h1 class="article-title">همه صفحات</h1>`;
| |
|
| |
| if (sorted.length === 0) {
| |
| html += `
| |
| <div class="ambox">
| |
| <span class="ambox-icon">📭</span>
| |
| <span>هنوز هیچ صفحهای وجود ندارد. <a href="#" onclick="openCreateModal(); return false;">اولین صفحه را بسازید</a>.</span>
| |
| </div>`;
| |
| } else {
| |
| html += `<p>تعداد کل صفحات: <b>${sorted.length}</b></p>`;
| |
| html += `<ul class="special-list">`;
| |
| sorted.forEach(p => {
| |
| const catText = p.categories.length > 0 ? ` — ${p.categories.join(', ')}` : '';
| |
| html += `<li><a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a> <span class="desc">${catText} (${formatDate(p.updatedAt)})</span></li>`;
| |
| });
| |
| html += `</ul>`;
| |
| }
| |
|
| |
| 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 = `<div class="toc"><div class="toc-title">فهرست مطالب <span class="toc-toggle" onclick="this.parentElement.querySelector('ol').classList.toggle('hidden');">[隐蔽]</span></div><ol>`;
| |
| 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 += `<li ${indent}><a href="#section-${i}">${text}</a></li>`;
| |
| });
| |
| tocHtml += `</ol></div>`;
| |
| }
| |
|
| |
| // Build categories
| |
| let catHtml = '';
| |
| if (page.categories.length > 0) {
| |
| catHtml = `<div class="catlinks"><strong>ردهها:</strong> ${page.categories.map(c => `<a href="#" onclick="filterByCategory('${escapeHtml(c)}'); return false;">${escapeHtml(c)}</a>`).join(' ')}</div>`;
| |
| }
| |
|
| |
| // Infobox
| |
| const wordCount = page.content.split(/\s+/).length;
| |
| const charCount = page.content.length;
| |
|
| |
| const infobox = `
| |
| <div class="infobox">
| |
| <div class="infobox-header">${escapeHtml(page.title)}</div>
| |
| <table>
| |
| <tr><th>تاریخ ایجاد</th><td>${formatDate(page.createdAt)}</td></tr>
| |
| <tr><th>آخرین ویرایش</th><td>${formatDate(page.updatedAt)}</td></tr>
| |
| <tr><th>تعداد کلمات</th><td>${wordCount.toLocaleString('fa-IR')}</td></tr>
| |
| <tr><th>تعداد حروف</th><td>${charCount.toLocaleString('fa-IR')}</td></tr>
| |
| ${page.categories.length > 0 ? `<tr><th>ردهها</th><td>${page.categories.join('، ')}</td></tr>` : ''}
| |
| </table>
| |
| </div>`;
| |
|
| |
| // Assemble
| |
| let fullHtml = `<h1 class="article-title">${escapeHtml(page.title)}</h1>`;
| |
| fullHtml += infobox;
| |
| fullHtml += tocHtml;
| |
| fullHtml += parsedContent;
| |
| fullHtml += `<div style="clear:both;"></div>`;
| |
| 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 += `<h2 style="font-weight:400;font-size:1.3rem;margin:1.5rem 0 0.5rem;border-bottom:1px solid var(--border-light);padding-bottom:0.2rem;">همچنین ببینید</h2>`;
| |
| fullHtml += `<ul style="margin:0.5rem 0;">`;
| |
| otherPages.forEach(p => {
| |
| fullHtml += `<li><a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a></li>`;
| |
| });
| |
| fullHtml += `</ul>`;
| |
| }
| |
|
| |
| 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 = `<h1 class="article-title" style="margin-bottom:1rem;">${page ? 'ویرایش: ' + escapeHtml(page.title) : 'ایجاد صفحه جدید'}</h1>`;
| |
|
| |
| html += `
| |
| <div style="background:var(--toc-bg);border:1px solid var(--border-light);padding:0.8rem;margin-bottom:1rem;font-size:0.88rem;">
| |
| <b>راهنمای سریع Wiki markup:</b><br>
| |
| <code>== عنوان ==</code> · <code>'''بولد'''</code> · <code>''ایتالیک''</code> · <code>[[پیوند]]</code> · <code>{{یادآوری|متن}}</code> · <code>* لیست</code> · <code><code>kod</code></code>
| |
| </div>
| |
| <div class="form-group">
| |
| <label>عنوان صفحه</label>
| |
| <input type="text" id="editTitle" value="${page ? escapeHtml(page.title) : ''}" placeholder="عنوان صفحه">
| |
| </div>
| |
| <div class="form-group">
| |
| <label>ردهها (با کاما جدا کنید)</label>
| |
| <input type="text" id="editCategories" value="${page ? page.categories.join(', ') : ''}" placeholder="برنامهنویسی, یادداشت">
| |
| </div>
| |
| <div class="form-group">
| |
| <label>محتوا</label>
| |
| <textarea id="editContent" placeholder="محتوای صفحه را اینجا بنویسید...">${page ? page.content : ''}</textarea>
| |
| </div>
| |
| <div style="display:flex;gap:0.5rem;">
| |
| <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>` : ''}
| |
| </div>`;
| |
|
| |
| 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 = `<li><a href="#" class="${!currentPageId ? 'active-page' : ''}" onclick="showMainPage(); return false;">صفحه اصلی</a></li>`;
| |
| recent.forEach(p => {
| |
| html += `<li><a href="#" class="${currentPageId === p.id ? 'active-page' : ''}" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a></li>`;
| |
| });
| |
| navPages.innerHTML = html;
| |
|
| |
| // Categories
| |
| const allCats = [...new Set(pages.flatMap(p => p.categories))].sort();
| |
| const catList = document.getElementById('categoryList');
| |
| catList.innerHTML = allCats.map(c =>
| |
| `<li><a href="#" onclick="filterByCategory('${escapeHtml(c)}'); return false;">${escapeHtml(c)}</a></li>`
| |
| ).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 = `<h1 class="article-title">رده: ${escapeHtml(cat)}</h1>`;
| |
| html += `<p>تعداد صفحات در این رده: <b>${matching.length}</b></p>`;
| |
|
| |
| if (matching.length > 0) {
| |
| html += `<ul class="special-list">`;
| |
| matching.forEach(p => {
| |
| html += `<li><a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a> <span class="desc">(${formatDate(p.updatedAt)})</span></li>`;
| |
| });
| |
| html += `</ul>`;
| |
| }
| |
|
| |
| 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 = `<h1 class="article-title">نتایج جستجو: «${escapeHtml(query)}»</h1>`;
| |
| html += `<p>${results.length} نتیجه یافت شد.</p>`;
| |
|
| |
| if (results.length > 0) {
| |
| html += `<ul class="special-list">`;
| |
| 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 += `<li><a href="#" onclick="navigateToPage('${escapeHtml(p.title)}'); return false;">${escapeHtml(p.title)}</a>`;
| |
| if (snippet) html += `<br><span class="desc">${escapeHtml(snippet)}</span>`;
| |
| html += `</li>`;
| |
| });
| |
| html += `</ul>`;
| |
| }
| |
|
| |
| 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>
| |