Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no"> | |
| <title>FynWrite - Blog Platform</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; } | |
| :root { | |
| --primary: #667eea; | |
| --secondary: #764ba2; | |
| --bg: #1a1a2e; | |
| --card-bg: rgba(255, 255, 255, 0.05); | |
| --text: #fff; | |
| --text-dim: rgba(255, 255, 255, 0.6); | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Unified LunID Auth Styles */ | |
| .lunid-auth-container, .lunid-picker-container { | |
| width: 100%; | |
| max-width: 360px; | |
| margin: 0 auto; | |
| text-align: left; | |
| } | |
| .lunid-header { text-align: center; margin-bottom: 24px; } | |
| .lunid-logo { margin-bottom: 16px; display: flex; justify-content: center; } | |
| .lunid-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; color: #fff; } | |
| .lunid-subtitle { font-size: 14px; color: rgba(255,255,255,0.6); } | |
| .lunid-tabs { display: flex; gap: 8px; margin-bottom: 20px; background: rgba(255,255,255,0.05); padding: 4px; border-radius: 12px; } | |
| .lunid-tab { | |
| flex: 1; padding: 10px; border-radius: 10px; border: none; | |
| background: transparent; color: rgba(255,255,255,0.6); | |
| font-weight: 500; cursor: pointer; transition: all 0.2s; | |
| } | |
| .lunid-tab.active { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; } | |
| .lunid-field { margin-bottom: 16px; } | |
| .lunid-field label { display: block; font-size: 13px; color: rgba(255,255,255,0.7); margin-bottom: 8px; } | |
| .lunid-field input { | |
| width: 100%; padding: 12px 16px; background: rgba(255,255,255,0.08); | |
| border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; | |
| color: #fff; font-size: 15px; outline: none; transition: all 0.2s; | |
| } | |
| .lunid-field input:focus { border-color: var(--primary); background: rgba(255,255,255,0.12); } | |
| .lunid-submit { | |
| width: 100%; background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; border: none; padding: 14px; border-radius: 12px; | |
| font-weight: 600; cursor: pointer; transition: all 0.2s; | |
| } | |
| .lunid-submit:disabled { opacity: 0.6; cursor: not-allowed; } | |
| .lunid-error { color: #f87171; font-size: 13px; margin-bottom: 12px; min-height: 18px; text-align: center; } | |
| .lunid-hint { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 4px; } | |
| /* Account Picker Styles */ | |
| .lunid-account-list { display: flex; flex-direction: column; gap: 12px; } | |
| .lunid-account-item { | |
| display: flex; align-items: center; gap: 12px; padding: 16px; | |
| background: rgba(255,255,255,0.05); border-radius: 16px; | |
| cursor: pointer; border: 1px solid transparent; transition: all 0.2s; | |
| } | |
| .lunid-account-item:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.1); } | |
| .lunid-account-avatar { | |
| width: 40px; height: 40px; border-radius: 50%; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| display: flex; align-items: center; justify-content: center; font-weight: 700; | |
| } | |
| .lunid-account-info { flex: 1; } | |
| .lunid-account-name { font-size: 15px; font-weight: 600; } | |
| .lunid-account-id { font-size: 12px; color: rgba(255,255,255,0.5); } | |
| .lunid-cancel { | |
| width: 100%; background: transparent; border: none; color: rgba(255,255,255,0.5); | |
| padding: 12px; cursor: pointer; font-size: 14px; margin-top: 8px; | |
| } | |
| /* Onboarding Styles */ | |
| .onboarding-overlay { | |
| position: fixed; inset: 0; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | |
| z-index: 2000; display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; padding: 24px; text-align: center; | |
| } | |
| .onboarding-card { | |
| background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); | |
| padding: 32px; border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.1); | |
| max-width: 400px; width: 100%; | |
| } | |
| .onboarding-icon { font-size: 64px; margin-bottom: 16px; } | |
| .onboarding-title { | |
| font-size: 24px; font-weight: 700; margin-bottom: 12px; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .onboarding-text { color: rgba(255, 255, 255, 0.7); line-height: 1.6; margin-bottom: 24px; } | |
| /* Auth Tabs (FynPay style) */ | |
| .auth-tabs { display: flex; gap: 8px; margin-bottom: 16px; background: rgba(255,255,255,0.05); padding: 4px; border-radius: 12px; } | |
| .auth-tab { | |
| flex: 1; padding: 10px; border-radius: 10px; border: none; | |
| background: transparent; color: rgba(255,255,255,0.6); | |
| font-weight: 500; cursor: pointer; transition: all 0.2s; | |
| } | |
| .auth-tab.active { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; box-shadow: 0 4px 12px rgba(102,126,234,0.3); } | |
| .form-control { | |
| width: 100%; padding: 14px; background: rgba(255,255,255,0.08); | |
| border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; | |
| color: #fff; font-size: 15px; outline: none; margin-bottom: 12px; | |
| transition: all 0.2s; | |
| } | |
| .form-control:focus { border-color: var(--primary); background: rgba(255,255,255,0.12); } | |
| .domain-picker { display: grid; gap: 12px; margin-top: 20px; } | |
| .domain-option { | |
| background: rgba(255, 255, 255, 0.08); padding: 16px; border-radius: 12px; | |
| text-align: left; cursor: pointer; border: 1px solid transparent; transition: all 0.2s; | |
| display: flex; justify-content: space-between; align-items: center; | |
| } | |
| .domain-option:hover { background: rgba(255, 255, 255, 0.15); border-color: var(--primary); } | |
| .domain-option.selected { background: rgba(102, 126, 234, 0.2); border-color: var(--primary); } | |
| .domain-name { font-weight: 600; } | |
| .domain-price { font-size: 12px; color: #4ade80; font-weight: 600; } | |
| /* Main App Layout */ | |
| .app-container { display: flex; height: 100vh; overflow: hidden; position: relative; } | |
| .sidebar { | |
| width: 260px; background: rgba(0,0,0,0.3); border-right: 1px solid rgba(255,255,255,0.1); | |
| display: flex; flex-direction: column; padding: 20px; transition: transform 0.3s ease; | |
| } | |
| .sidebar-logo { | |
| font-size: 24px; font-weight: 700; margin-bottom: 30px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .nav-item { | |
| display: flex; align-items: center; gap: 12px; padding: 12px 16px; | |
| border-radius: 12px; color: var(--text-dim); cursor: pointer; | |
| margin-bottom: 4px; transition: all 0.2s; | |
| } | |
| .nav-item:hover, .nav-item.active { background: rgba(102, 126, 234, 0.1); color: #fff; } | |
| .nav-item.active { background: var(--primary); } | |
| .add-post-btn { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; border: none; padding: 14px; border-radius: 12px; | |
| font-weight: 600; cursor: pointer; margin-bottom: 20px; | |
| display: flex; align-items: center; justify-content: center; gap: 8px; | |
| transition: transform 0.2s; | |
| } | |
| .add-post-btn:active { transform: scale(0.98); } | |
| .main-content { flex: 1; overflow-y: auto; display: flex; flex-direction: column; width: 100%; } | |
| .top-bar { | |
| padding: 12px 20px; display: flex; justify-content: space-between; align-items: center; | |
| background: rgba(0,0,0,0.2); backdrop-filter: blur(10px); position: sticky; top: 0; z-index: 10; | |
| } | |
| .menu-toggle { display: none; background: none; border: none; color: white; font-size: 24px; cursor: pointer; } | |
| @media (max-width: 768px) { | |
| .sidebar { position: absolute; left: 0; top: 0; bottom: 0; z-index: 100; transform: translateX(-100%); background: #1a1a2e; } | |
| .sidebar.show { transform: translateX(0); } | |
| .menu-toggle { display: block; } | |
| .onboarding-card { padding: 24px; } | |
| } | |
| /* Post List V2 */ | |
| .post-filters { | |
| display: flex; gap: 12px; margin-bottom: 24px; overflow-x: auto; padding-bottom: 8px; | |
| position: sticky; top: 60px; background: var(--bg); z-index: 9; | |
| } | |
| .filter-chip { | |
| padding: 8px 16px; border-radius: 20px; background: rgba(255,255,255,0.05); | |
| color: var(--text-dim); cursor: pointer; white-space: nowrap; border: 1px solid transparent; | |
| transition: all 0.2s; font-size: 14px; | |
| } | |
| .filter-chip.active { background: var(--primary); color: #fff; } | |
| .filter-count { opacity: 0.6; margin-left: 4px; font-size: 12px; } | |
| .posts-grid { display: grid; gap: 16px; } | |
| .post-card { | |
| background: #fff; border-radius: 12px; overflow: hidden; display: flex; | |
| color: #333; transition: transform 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| .post-card-thumb { width: 120px; min-height: 120px; background: #eee; position: relative; flex-shrink: 0; } | |
| .post-card-thumb img { width: 100%; height: 100%; object-fit: cover; } | |
| .post-badge { | |
| position: absolute; top: 8px; right: 8px; padding: 2px 8px; border-radius: 4px; | |
| font-size: 10px; font-weight: 700; text-transform: uppercase; | |
| } | |
| .badge-draft { background: #ffd60a; color: #000; } | |
| .badge-published { background: #4ade80; color: #fff; } | |
| .badge-scheduled { background: #667eea; color: #fff; } | |
| .post-card-content { flex: 1; padding: 16px; display: flex; flex-direction: column; justify-content: center; position: relative; } | |
| .post-card-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; line-height: 1.3; padding-right: 24px; color: #1a1a2e; } | |
| .post-card-meta { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #666; } | |
| .post-card-stats { display: flex; align-items: center; gap: 12px; margin-top: 12px; } | |
| .stat-item { display: flex; align-items: center; gap: 4px; font-size: 13px; color: #888; } | |
| .post-options-btn { | |
| position: absolute; right: 8px; top: 12px; background: none; border: none; | |
| color: #666; font-size: 20px; cursor: pointer; padding: 4px; | |
| } | |
| .fab-container { position: fixed; bottom: 24px; right: 24px; z-index: 100; } | |
| .fab-btn { | |
| width: 56px; height: 56px; border-radius: 28px; background: #ff5722; | |
| color: white; border: none; display: flex; align-items: center; justify-content: center; | |
| font-size: 24px; cursor: pointer; box-shadow: 0 4px 12px rgba(255,87,34,0.4); | |
| } | |
| .dropdown-menu { | |
| position: absolute; right: 10px; top: 40px; background: #fff; border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); display: none; flex-direction: column; | |
| z-index: 100; border: 1px solid #eee; overflow: hidden; | |
| } | |
| .dropdown-item { | |
| padding: 10px 16px; font-size: 14px; color: #333; cursor: pointer; | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .dropdown-item:hover { background: #f5f5f5; } | |
| .view-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } | |
| .view-title { font-size: 24px; font-weight: 700; color: #fff; } | |
| .editor-overlay { position: fixed; inset: 0; background: var(--bg); z-index: 1000; display: none; flex-direction: column; } | |
| .editor-header { padding: 12px 20px; border-bottom: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: space-between; align-items: center; } | |
| .editor-body { flex: 1; padding: 20px; overflow-y: auto; } | |
| .editor-title { width: 100%; background: transparent; border: none; color: white; font-size: 24px; font-weight: 700; margin-bottom: 20px; outline: none; } | |
| .editor-content { width: 100%; min-height: 300px; background: transparent; border: none; color: white; font-size: 16px; line-height: 1.6; outline: none; resize: none; } | |
| .header-btn { background: rgba(255,255,255,0.1); border: none; color: #fff; padding: 8px 12px; border-radius: 8px; cursor: pointer; font-size: 14px; } | |
| .header-btn.primary { background: linear-gradient(135deg, #667eea, #764ba2); } | |
| /* Block Editor V2 Styles */ | |
| .block-container { display: flex; flex-direction: column; gap: 16px; margin-top: 20px; padding-bottom: 100px; } | |
| .section-block { | |
| border: 2px dashed rgba(255,255,255,0.1); border-radius: 16px; padding: 20px; | |
| background: rgba(255,255,255,0.02); margin-bottom: 20px; | |
| } | |
| .container-block { | |
| max-width: 1200px; margin: 0 auto; border: 1px dashed rgba(255,255,255,0.1); | |
| padding: 15px; border-radius: 12px; | |
| } | |
| .columns-block { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } | |
| .block-item { | |
| background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 12px; padding: 12px; position: relative; transition: all 0.2s; | |
| margin-bottom: 8px; | |
| } | |
| .block-item:hover { border-color: var(--primary); background: rgba(255, 255, 255, 0.05); } | |
| .block-controls { | |
| position: absolute; right: 8px; top: -12px; display: flex; gap: 4px; | |
| opacity: 0; transition: opacity 0.2s; z-index: 10; | |
| } | |
| .block-item:hover .block-controls { opacity: 1; } | |
| .block-label { | |
| position: absolute; left: 12px; top: -10px; font-size: 10px; | |
| background: var(--primary); color: white; padding: 2px 6px; | |
| border-radius: 4px; text-transform: uppercase; font-weight: 700; | |
| } | |
| .block-input { | |
| width: 100%; background: transparent; border: none; color: #fff; | |
| font-size: 16px; outline: none; resize: none; font-family: inherit; | |
| } | |
| .block-header-input { font-size: 24px; font-weight: 700; } | |
| /* Specialized Blocks */ | |
| .hero-editor { padding: 40px 20px; text-align: center; background: rgba(102, 126, 234, 0.1); border-radius: 12px; } | |
| .navbar-editor { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px; } | |
| .spacer-editor { height: 40px; border-top: 1px dashed rgba(255,255,255,0.2); border-bottom: 1px dashed rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 12px; color: var(--text-dim); } | |
| .add-block-menu { | |
| position: sticky; bottom: 20px; left: 0; right: 0; | |
| display: flex; gap: 8px; padding: 16px; background: rgba(26, 26, 46, 0.9); | |
| backdrop-filter: blur(10px); border-radius: 16px; margin: 20px; | |
| border: 1px solid rgba(255,255,255,0.1); justify-content: center; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 100; flex-wrap: wrap; | |
| } | |
| .add-block-btn { | |
| background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #fff; | |
| padding: 8px 12px; border-radius: 8px; cursor: pointer; font-size: 12px; | |
| display: flex; align-items: center; gap: 6px; transition: all 0.2s; | |
| } | |
| .add-block-btn:hover { background: var(--primary); border-color: var(--primary); transform: translateY(-2px); } | |
| .btn-group-label { width: 100%; font-size: 10px; text-transform: uppercase; color: var(--text-dim); text-align: center; margin-bottom: 4px; } | |
| /* Stats View */ | |
| .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 20px; } | |
| .stat-card { background: var(--card-bg); padding: 24px; border-radius: 20px; text-align: center; border: 1px solid rgba(255,255,255,0.05); } | |
| .stat-value { font-size: 32px; font-weight: 800; margin-bottom: 8px; color: var(--primary); } | |
| .stat-label { font-size: 14px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; } | |
| /* Themes View */ | |
| .themes-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 20px; } | |
| .theme-card { | |
| background: var(--card-bg); border-radius: 20px; overflow: hidden; border: 2px solid transparent; | |
| cursor: pointer; transition: all 0.3s ease; position: relative; | |
| } | |
| .theme-card.active { border-color: var(--primary); transform: translateY(-4px); } | |
| .theme-preview { height: 120px; background: #333; } | |
| .theme-info { padding: 16px; } | |
| .theme-name { font-weight: 700; margin-bottom: 4px; } | |
| .theme-desc { font-size: 13px; color: var(--text-dim); } | |
| /* Settings View */ | |
| .settings-section { background: var(--card-bg); border-radius: 20px; padding: 24px; margin-top: 20px; border: 1px solid rgba(255,255,255,0.05); } | |
| .settings-title { font-size: 18px; font-weight: 700; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; } | |
| .settings-row { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid rgba(255,255,255,0.05); } | |
| .settings-row:last-child { border-bottom: none; } | |
| .settings-label { flex: 1; } | |
| .settings-label div:first-child { font-weight: 600; margin-bottom: 4px; } | |
| .settings-label div:last-child { font-size: 12px; color: var(--text-dim); } | |
| .switch { position: relative; display: inline-block; width: 44px; height: 24px; } | |
| .switch input { opacity: 0; width: 0; height: 0; } | |
| .slider { position: absolute; cursor: pointer; inset: 0; background-color: rgba(255,255,255,0.1); transition: .4s; border-radius: 34px; } | |
| .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } | |
| input:checked + .slider { background-color: var(--primary); } | |
| input:checked + .slider:before { transform: translateX(20px); } | |
| .view { display: none; padding: 20px; } | |
| .view.active { display: block; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Onboarding View --> | |
| <div id="onboardingView" class="onboarding-overlay"> | |
| <div class="onboarding-card"> | |
| <!-- Step 1: Welcome & Auth (FynPay style integrated) --> | |
| <div id="step1"> | |
| <div class="onboarding-icon">✍️</div> | |
| <h2 class="onboarding-title">Welcome to FynWrite</h2> | |
| <p class="onboarding-text" style="margin-bottom: 16px;">Sign in with your LunID to start your journey.</p> | |
| <div id="lunidAuthContainer"></div> | |
| <div id="authError" style="color: #f87171; font-size: 13px; margin-top: 12px; min-height: 18px;"></div> | |
| </div> | |
| <!-- Step 2: Website Info --> | |
| <div id="step2" style="display: none;"> | |
| <h2 class="onboarding-title">Website Name</h2> | |
| <p class="onboarding-text">What should we call your site?</p> | |
| <input type="text" id="newSiteName" class="form-control" placeholder="My Awesome Site"> | |
| <h2 class="onboarding-title" style="margin-top: 20px;">Domain Name</h2> | |
| <p class="onboarding-text">Choose your unique address.</p> | |
| <input type="text" id="newSiteDomain" class="form-control" placeholder="anime" oninput="checkDomainAvailability(this.value)"> | |
| <div id="domainHint" style="font-size: 12px; margin: -8px 0 16px; text-align: left;"></div> | |
| <button id="nextStepBtn" class="add-post-btn" style="width: 100%;" onclick="nextStep(3)" disabled>Next</button> | |
| </div> | |
| <!-- Step 3: Domain Selection (All options included) --> | |
| <div id="step3" style="display: none;"> | |
| <h2 class="onboarding-title">Confirm Address</h2> | |
| <p class="onboarding-text">Pick the suffix for your site.</p> | |
| <div class="domain-picker"> | |
| <div class="domain-option selected" onclick="selectDomain(this, 0, '.nexa.nx')"> | |
| <span class="domain-name">.nexa.nx</span> | |
| <span class="domain-price">Free</span> | |
| </div> | |
| <div class="domain-option" onclick="selectDomain(this, 0, '.fynwrite.nx')"> | |
| <span class="domain-name">.fynwrite.nx</span> | |
| <span class="domain-price">Free</span> | |
| </div> | |
| <div class="domain-option" onclick="selectDomain(this, 100, '.nx')"> | |
| <span class="domain-name">.nx</span> | |
| <span class="domain-price">100 FYN</span> | |
| </div> | |
| <div class="domain-option" onclick="selectDomain(this, 150, '.tom')"> | |
| <span class="domain-name">.tom</span> | |
| <span class="domain-price">150 FYN</span> | |
| </div> | |
| </div> | |
| <div id="finalDomainPreview" style="margin-top: 16px; font-size: 14px; font-weight: 600; color: var(--primary);"></div> | |
| <div style="display: flex; gap: 12px; margin-top: 24px;"> | |
| <button class="header-btn" style="flex: 1;" onclick="nextStep(2)">Back</button> | |
| <button class="add-post-btn" style="flex: 1; margin: 0;" onclick="finishOnboarding()">Create Site</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="app-container"> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-logo">FynWrite</div> | |
| <button class="add-post-btn" onclick="openEditor()"><span>+</span> New Post</button> | |
| <div class="nav-item active" onclick="showView('posts')">📝 Posts</div> | |
| <div class="nav-item" onclick="showView('stats')">📊 Stats</div> | |
| <div class="nav-item" onclick="showView('themes')">🎨 Theme</div> | |
| <div class="nav-item" onclick="showView('settings')">⚙️ Settings</div> | |
| </div> | |
| <div class="main-content"> | |
| <div class="top-bar"> | |
| <div style="display: flex; align-items: center; gap: 12px;"> | |
| <button class="menu-toggle" onclick="toggleSidebar()">☰</button> | |
| <h2 id="viewTitle" style="font-size: 18px;">Posts</h2> | |
| </div> | |
| <div id="userInfo"></div> | |
| </div> | |
| <div class="content-area"> | |
| <div id="postsView" class="view active"> | |
| <div class="post-filters"> | |
| <div class="filter-chip active" onclick="filterPosts('all')">All <span class="filter-count" id="countAll">(0)</span></div> | |
| <div class="filter-chip" onclick="filterPosts('published')">Published <span class="filter-count" id="countPublished">(0)</span></div> | |
| <div class="filter-chip" onclick="filterPosts('draft')">Draft <span class="filter-count" id="countDraft">(0)</span></div> | |
| <div class="filter-chip" onclick="filterPosts('scheduled')">Scheduled <span class="filter-count" id="countScheduled">(0)</span></div> | |
| <div class="filter-chip" onclick="filterPosts('trash')">Trash <span class="filter-count" id="countTrash">(0)</span></div> | |
| </div> | |
| <div id="postsList" class="posts-grid"> | |
| <!-- Posts injected here --> | |
| </div> | |
| <div class="fab-container"> | |
| <button class="fab-btn" onclick="openEditor()">+</button> | |
| </div> | |
| </div> | |
| <div id="statsView" class="view"> | |
| <h2 class="onboarding-title">Performance Overview</h2> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="statViews">1,234</div> | |
| <div class="stat-label">Total Views</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="statPosts">12</div> | |
| <div class="stat-label">Total Posts</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="statAvg">42</div> | |
| <div class="stat-label">Avg. Read Time</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="themesView" class="view"> | |
| <h2 class="onboarding-title">Choose Your Style</h2> | |
| <div class="themes-grid"> | |
| <div class="theme-card active" onclick="setTheme('default', this)"> | |
| <div class="theme-preview" style="background: #1a1a2e"></div> | |
| <div class="theme-info"> | |
| <div class="theme-name">Midnight Dark</div> | |
| <div class="theme-desc">The classic FynWrite experience.</div> | |
| </div> | |
| </div> | |
| <div class="theme-card" onclick="setTheme('light', this)"> | |
| <div class="theme-preview" style="background: #f8f9fa"></div> | |
| <div class="theme-info"> | |
| <div class="theme-name">Paper White</div> | |
| <div class="theme-desc">Clean and minimalist for focused reading.</div> | |
| </div> | |
| </div> | |
| <div class="theme-card" onclick="setTheme('sunset', this)"> | |
| <div class="theme-preview" style="background: linear-gradient(135deg, #764ba2, #667eea)"></div> | |
| <div class="theme-info"> | |
| <div class="theme-name">Cosmic Sunset</div> | |
| <div class="theme-desc">Vibrant gradients for a bold look.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="settingsView" class="view"> | |
| <div class="settings-section"> | |
| <div class="settings-title">⚙️ Basic Site Settings</div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Site Title</div> | |
| <div>Main name of your blog</div> | |
| </div> | |
| <input type="text" id="setting_siteTitle" class="form-control" style="width:200px; margin:0" placeholder="My Blog"> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Site Description</div> | |
| <div>Short bio or summary</div> | |
| </div> | |
| <textarea id="setting_siteDesc" class="form-control" style="width:200px; margin:0; height:60px" placeholder="Write something..."></textarea> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Site Language</div> | |
| <div>Default content language</div> | |
| </div> | |
| <select id="setting_siteLang" class="form-control" style="width:200px; margin:0"> | |
| <option value="en">English</option> | |
| <option value="es">Español</option> | |
| <option value="fr">Français</option> | |
| </select> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Content Warning</div> | |
| <div>Show a warning before viewing</div> | |
| </div> | |
| <label class="switch"> | |
| <input type="checkbox" id="setting_contentWarning"> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-title">🌐 Domain & Publishing</div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Site Name (Slug)</div> | |
| <div>Example: anime</div> | |
| </div> | |
| <input type="text" id="setting_siteSlug" class="form-control" style="width:200px; margin:0"> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Generated URL</div> | |
| <div>Public Nexa address</div> | |
| </div> | |
| <input type="text" id="setting_siteUrl" class="form-control" style="width:200px; margin:0" readonly> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Publish Status</div> | |
| <div>Live on Nexa browser</div> | |
| </div> | |
| <label class="switch"> | |
| <input type="checkbox" id="setting_isPublished" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-title">🔍 SEO & Discovery</div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Nexa Search Indexing</div> | |
| <div>Allow others to find your site</div> | |
| </div> | |
| <label class="switch"> | |
| <input type="checkbox" id="setting_allowIndexing" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Meta Title</div> | |
| <div>SEO Title tag</div> | |
| </div> | |
| <input type="text" id="setting_metaTitle" class="form-control" style="width:200px; margin:0"> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-title">📸 Images & Performance</div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Lazy Loading</div> | |
| <div>Speed up page load</div> | |
| </div> | |
| <label class="switch"> | |
| <input type="checkbox" id="setting_lazyLoad" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Image Lightbox</div> | |
| <div>Click images to expand</div> | |
| </div> | |
| <label class="switch"> | |
| <input type="checkbox" id="setting_lightbox" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-title">💬 Comments</div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Enable Comments</div> | |
| <div>Allow users to leave feedback</div> | |
| </div> | |
| <label class="switch"> | |
| <input type="checkbox" id="setting_enableComments"> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-title">💻 Custom Code</div> | |
| <div class="settings-row"> | |
| <div class="settings-label"> | |
| <div>Header Code</div> | |
| <div>Injected in <head></div> | |
| </div> | |
| <textarea id="setting_customHead" class="form-control" style="width:200px; margin:0; height:60px"></textarea> | |
| </div> | |
| </div> | |
| <button class="add-post-btn" style="width:100%; margin-top:20px" onclick="saveSettings()">Save All Settings</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="editorOverlay" class="editor-overlay"> | |
| <div class="editor-header"> | |
| <button class="icon-btn" onclick="saveDraft()" style="font-size: 24px">✕</button> | |
| <button class="add-post-btn" style="margin: 0" onclick="createPost()">Publish</button> | |
| </div> | |
| <div class="editor-body"> | |
| <input type="text" id="postTitle" class="editor-title" placeholder="Post Title"> | |
| <div id="blocksContainer" class="block-container"> | |
| <!-- Blocks will be added here --> | |
| </div> | |
| <div class="add-block-menu"> | |
| <div class="btn-group-label">Layout</div> | |
| <button class="add-block-btn" onclick="addBlock('section')"><span>⬛</span> Section</button> | |
| <button class="add-block-btn" onclick="addBlock('columns')"><span>⫫</span> Columns</button> | |
| <button class="add-block-btn" onclick="addBlock('spacer')"><span>↕️</span> Spacer</button> | |
| <div class="btn-group-label">Content</div> | |
| <button class="add-block-btn" onclick="addBlock('header')"><span>H</span> Heading</button> | |
| <button class="add-block-btn" onclick="addBlock('paragraph')"><span>P</span> Text</button> | |
| <button class="add-block-btn" onclick="addBlock('button')"><span>🔘</span> Button</button> | |
| <button class="add-block-btn" onclick="addBlock('html')"><span><></span> HTML</button> | |
| <div class="btn-group-label">Pre-built</div> | |
| <button class="add-block-btn" onclick="addBlock('navbar')"><span>🔝</span> Navbar</button> | |
| <button class="add-block-btn" onclick="addBlock('hero')"><span>✨</span> Hero</button> | |
| <button class="add-block-btn" onclick="addBlock('footer')"><span>🔚</span> Footer</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="/static/lunid-auth.js"></script> | |
| <script> | |
| const auth = new LunIDAuth(); | |
| let authToken = auth.accessToken; | |
| // Auto-login check and sync | |
| window.addEventListener('DOMContentLoaded', () => { | |
| auth.createLoginUI('lunidAuthContainer', { | |
| appName: 'FynWrite', | |
| onSuccess: (user) => { | |
| authToken = auth.accessToken; | |
| syncAuth(); | |
| } | |
| }); | |
| syncAuth(); | |
| // Re-check on auth changes | |
| auth.onAuthChange((isAuthenticated, user) => { | |
| syncAuth(); | |
| }); | |
| }); | |
| function syncAuth() { | |
| if (auth.isAuthenticated) { | |
| // If user is authenticated, check if they already have a site | |
| const savedSiteDomain = localStorage.getItem('fynwrite_site_domain'); | |
| if (savedSiteDomain) { | |
| document.getElementById('onboardingView').style.display = 'none'; | |
| showView('posts'); | |
| loadPosts(); | |
| } else { | |
| // authenticated but no site, show domain creation (Step 2) | |
| document.getElementById('onboardingView').style.display = 'flex'; | |
| nextStep(2); | |
| } | |
| } else { | |
| document.getElementById('onboardingView').style.display = 'flex'; | |
| nextStep(1); | |
| } | |
| } | |
| async function showAuthTab(tab) { | |
| document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active')); | |
| if (tab === 'login') { | |
| document.getElementById('loginTabBtn').classList.add('active'); | |
| document.getElementById('loginForm').style.display = 'block'; | |
| document.getElementById('signupForm').style.display = 'none'; | |
| } else { | |
| document.getElementById('signupTabBtn').classList.add('active'); | |
| document.getElementById('loginForm').style.display = 'none'; | |
| document.getElementById('signupForm').style.display = 'block'; | |
| } | |
| } | |
| async function handleLogin() { | |
| const user = document.getElementById('loginId').value; | |
| const pass = document.getElementById('loginPw').value; | |
| if(!user || !pass) return document.getElementById('authError').innerText = 'Fill all fields'; | |
| const btn = event.target; | |
| btn.disabled = true; | |
| btn.innerText = 'Signing in...'; | |
| const result = await auth.login(user, pass); | |
| if (result.success) { | |
| authToken = auth.accessToken; | |
| nextStep(2); | |
| } else { | |
| document.getElementById('authError').innerText = result.error || 'Login failed'; | |
| } | |
| btn.disabled = false; | |
| btn.innerText = 'Sign In'; | |
| } | |
| async function handleSignup() { | |
| const user = document.getElementById('signupUser').value; | |
| const pass = document.getElementById('signupPw').value; | |
| if(!user || pass.length < 6) return document.getElementById('authError').innerText = 'Username and 6+ char password required'; | |
| const btn = event.target; | |
| btn.disabled = true; | |
| btn.innerText = 'Creating account...'; | |
| const result = await auth.signup(user, pass); | |
| if (result.success) { | |
| authToken = auth.accessToken; | |
| nextStep(2); | |
| } else { | |
| document.getElementById('authError').innerText = result.error || 'Signup failed'; | |
| } | |
| btn.disabled = false; | |
| btn.innerText = 'Create LunID Account'; | |
| } | |
| async function checkUsername(username) { | |
| if (username.length < 3) return; | |
| const hint = document.getElementById('usernameHint'); | |
| const res = await auth.checkUsername(username); | |
| if (res.available) { | |
| hint.textContent = `${username}@lunos is available!`; | |
| hint.style.color = '#4ade80'; | |
| } else { | |
| hint.textContent = res.error || 'Not available'; | |
| hint.style.color = '#f87171'; | |
| } | |
| } | |
| function nextStep(step) { | |
| ['step1', 'step2', 'step3'].forEach((id, idx) => { | |
| document.getElementById(id).style.display = (idx + 1 === step) ? 'block' : 'none'; | |
| }); | |
| } | |
| let selectedDomainSuffix = '.nexa.nx'; | |
| let currentFullDomain = localStorage.getItem('fynwrite_site_domain') || ''; | |
| async function checkDomainAvailability(val) { | |
| const hint = document.getElementById('domainHint'); | |
| const nextBtn = document.getElementById('nextStepBtn'); | |
| const domain = val.trim().toLowerCase(); | |
| if (domain.length < 3) { | |
| hint.textContent = 'Minimum 3 characters'; | |
| hint.style.color = 'var(--text-dim)'; | |
| nextBtn.disabled = true; | |
| return; | |
| } | |
| try { | |
| const fullDomain = domain + selectedDomainSuffix; | |
| console.log('Checking availability for:', fullDomain); | |
| const url = `/api/nexa/websites/check-availability?domain=${encodeURIComponent(fullDomain)}`; | |
| const res = await fetch(url); | |
| if (!res.ok) { | |
| let errorMsg = `Error: ${res.status}`; | |
| try { | |
| const errorData = await res.json(); | |
| errorMsg = errorData.error || errorMsg; | |
| } catch (e) {} | |
| console.error('Server returned error status:', res.status); | |
| hint.textContent = errorMsg; | |
| hint.style.color = '#f87171'; | |
| nextBtn.disabled = true; | |
| return; | |
| } | |
| const data = await res.json(); | |
| console.log('Availability data:', data); | |
| if (data.success || data.available !== undefined) { | |
| if (data.available) { | |
| hint.textContent = `${fullDomain} is available!`; | |
| hint.style.color = '#4ade80'; | |
| nextBtn.disabled = false; | |
| currentFullDomain = fullDomain; | |
| } else { | |
| hint.textContent = `${fullDomain} is already taken`; | |
| hint.style.color = '#f87171'; | |
| nextBtn.disabled = true; | |
| } | |
| } else { | |
| hint.textContent = data.error || 'Check failed'; | |
| hint.style.color = '#f87171'; | |
| nextBtn.disabled = true; | |
| } | |
| } catch (e) { | |
| console.error('Domain check fetch error:', e); | |
| hint.textContent = 'Connection error. Please try again.'; | |
| hint.style.color = '#f87171'; | |
| nextBtn.disabled = true; | |
| } | |
| } | |
| // Add this function if missing, or update if exists | |
| function updateFullDomain() { | |
| const domainInput = document.getElementById('newSiteDomain'); | |
| if (domainInput) { | |
| const domain = domainInput.value.trim().toLowerCase(); | |
| if (domain.length >= 3) { | |
| currentFullDomain = domain + selectedDomainSuffix; | |
| } | |
| } | |
| } | |
| function selectDomain(el, price, suffix) { | |
| document.querySelectorAll('.domain-option').forEach(o => o.classList.remove('selected')); | |
| el.classList.add('selected'); | |
| selectedDomainSuffix = suffix; | |
| selectedPrice = price; | |
| const domainInput = document.getElementById('newSiteDomain').value; | |
| if (domainInput) { | |
| checkDomainAvailability(domainInput); | |
| } | |
| } | |
| function updatePreview() { | |
| const preview = document.getElementById('finalDomainPreview'); | |
| if (preview) { | |
| let text = `Address: nexs://${currentFullDomain}`; | |
| if (selectedPrice > 0) text += ` (${selectedPrice} FYN)`; | |
| preview.textContent = text; | |
| } | |
| } | |
| async function finishOnboarding() { | |
| const name = document.getElementById('newSiteName').value; | |
| if (!name || !currentFullDomain) return alert('Please complete all steps'); | |
| if (selectedPrice > 0) { | |
| if (!confirm(`This domain costs ${selectedPrice} FYN. Pay now?`)) return; | |
| } | |
| try { | |
| console.log('Creating site with:', { name, domain: currentFullDomain, price: selectedPrice }); | |
| const res = await auth.fetchWithAuth('/api/nexa/websites', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| name: name, | |
| domain: currentFullDomain, | |
| price: selectedPrice | |
| }) | |
| }); | |
| if (!res.ok) { | |
| let errorMsg = `Error: ${res.status}`; | |
| try { | |
| const errorData = await res.json(); | |
| errorMsg = errorData.error || errorMsg; | |
| } catch (e) {} | |
| alert(errorMsg); | |
| return; | |
| } | |
| const data = await res.json(); | |
| if (data.success) { | |
| localStorage.setItem('fynwrite_site_domain', currentFullDomain); | |
| localStorage.setItem('fynwrite_site_name', name); | |
| // Small delay to ensure DB sync before redirecting | |
| setTimeout(() => { | |
| document.getElementById('onboardingView').style.display = 'none'; | |
| showView('posts'); | |
| loadPosts(); | |
| }, 1000); | |
| } else { | |
| alert(data.error || 'Failed to create site'); | |
| } | |
| } catch (e) { | |
| console.error('Site creation error:', e); | |
| // Check if site was actually created despite the network error | |
| const checkRes = await fetch(`/api/nexa/websites/${currentFullDomain}`).catch(() => null); | |
| if (checkRes && checkRes.ok) { | |
| localStorage.setItem('fynwrite_site_domain', currentFullDomain); | |
| localStorage.setItem('fynwrite_site_name', name); | |
| document.getElementById('onboardingView').style.display = 'none'; | |
| showView('posts'); | |
| } else { | |
| alert('Network error: Could not confirm site creation. Please refresh.'); | |
| } | |
| } | |
| } | |
| async function loadSettings() { | |
| const domain = localStorage.getItem('fynwrite_site_domain'); | |
| if (!domain) return; | |
| try { | |
| const res = await auth.fetchWithAuth(`/api/nexa/websites/${domain}`); | |
| if (res.ok) { | |
| const site = await res.json(); | |
| if (document.getElementById('setting_siteTitle')) document.getElementById('setting_siteTitle').value = site.name || ''; | |
| if (document.getElementById('setting_siteDesc')) document.getElementById('setting_siteDesc').value = site.description || ''; | |
| if (document.getElementById('setting_siteSlug')) document.getElementById('setting_siteSlug').value = site.domain ? site.domain.split('.')[0] : ''; | |
| if (document.getElementById('setting_siteUrl')) document.getElementById('setting_siteUrl').value = site.domain ? `nexs://${site.domain}` : ''; | |
| if (document.getElementById('setting_isPublished')) document.getElementById('setting_isPublished').checked = !!site.is_published; | |
| } | |
| } catch (e) { | |
| console.error('Error loading settings:', e); | |
| } | |
| } | |
| function showView(viewId) { | |
| document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); | |
| const view = document.getElementById(viewId + 'View'); | |
| if(view) view.classList.add('active'); | |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); | |
| const navItems = { | |
| 'posts': '📝 Posts', | |
| 'stats': '📊 Stats', | |
| 'themes': '🎨 Theme', | |
| 'settings': '⚙️ Settings' | |
| }; | |
| document.querySelectorAll('.nav-item').forEach(n => { | |
| if (n.innerText.includes(navItems[viewId])) n.classList.add('active'); | |
| }); | |
| // Set title and handle sidebar | |
| document.getElementById('viewTitle').innerText = viewId.charAt(0).toUpperCase() + viewId.slice(1); | |
| if(window.innerWidth <= 768) toggleSidebar(); | |
| // Load specific view data | |
| if (viewId === 'settings') loadSettings(); | |
| } | |
| async function saveSettings() { | |
| const domain = localStorage.getItem('fynwrite_site_domain'); | |
| if (!domain) return alert('Error: No active site found.'); | |
| const settings = { | |
| title: document.getElementById('setting_siteTitle')?.value || '', | |
| description: document.getElementById('setting_siteDesc')?.value || '', | |
| language: document.getElementById('setting_siteLang')?.value || 'en', | |
| content_warning: document.getElementById('setting_contentWarning')?.checked || false, | |
| slug: document.getElementById('setting_siteSlug')?.value || '', | |
| is_published: document.getElementById('setting_isPublished') ? document.getElementById('setting_isPublished').checked : true, | |
| allow_indexing: document.getElementById('setting_allowIndexing') ? document.getElementById('setting_allowIndexing').checked : true, | |
| meta_title: document.getElementById('setting_metaTitle')?.value || '', | |
| lazy_load: document.getElementById('setting_lazyLoad') ? document.getElementById('setting_lazyLoad').checked : true, | |
| lightbox: document.getElementById('setting_lightbox') ? document.getElementById('setting_lightbox').checked : true, | |
| enable_comments: document.getElementById('setting_enableComments')?.checked || false, | |
| custom_head: document.getElementById('setting_customHead')?.value || '' | |
| }; | |
| try { | |
| let res = await auth.fetchWithAuth(`/api/nexa/websites/${domain}/settings`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(settings) | |
| }); | |
| // If it's a 401/403 despite fetchWithAuth's refresh attempt, | |
| // it might be a specific error message being returned as JSON | |
| if (res.status === 401 || res.status === 403) { | |
| const errorData = await res.clone().json().catch(() => ({})); | |
| if (errorData.error && errorData.error.toLowerCase().includes('expired')) { | |
| // Force one manual refresh if the automatic one in fetchWithAuth didn't trigger correctly | |
| const refreshed = await auth.refreshToken(); | |
| if (refreshed) { | |
| res = await auth.fetchWithAuth(`/api/nexa/websites/${domain}/settings`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(settings) | |
| }); | |
| } | |
| } | |
| } | |
| if (!res.ok) { | |
| const errorData = await res.json().catch(() => ({})); | |
| throw new Error(errorData.error || `Server error: ${res.status}`); | |
| } | |
| alert('Settings saved!'); | |
| // Update local slug if changed | |
| if (settings.slug && !domain.startsWith(settings.slug + '.')) { | |
| const parts = domain.split('.'); | |
| const suffix = '.' + parts.slice(1).join('.'); | |
| const newDomain = settings.slug.trim().toLowerCase() + suffix; | |
| localStorage.setItem('fynwrite_site_domain', newDomain); | |
| document.getElementById('setting_siteUrl').value = `nexs://${newDomain}`; | |
| } | |
| } catch (e) { | |
| alert('Error: ' + e.message); | |
| } | |
| } | |
| function setTheme(theme, el) { | |
| document.querySelectorAll('.theme-card').forEach(c => c.classList.remove('active')); | |
| el.classList.add('active'); | |
| const root = document.documentElement; | |
| if (theme === 'light') { | |
| root.style.setProperty('--bg', '#f8f9fa'); | |
| root.style.setProperty('--text', '#1a1a2e'); | |
| root.style.setProperty('--card-bg', 'rgba(0,0,0,0.05)'); | |
| root.style.setProperty('--text-dim', 'rgba(0,0,0,0.6)'); | |
| } else if (theme === 'sunset') { | |
| root.style.setProperty('--bg', '#2d1b4e'); | |
| root.style.setProperty('--text', '#fff'); | |
| root.style.setProperty('--card-bg', 'rgba(255,255,255,0.1)'); | |
| root.style.setProperty('--text-dim', 'rgba(255,255,255,0.7)'); | |
| } else { | |
| root.style.setProperty('--bg', '#1a1a2e'); | |
| root.style.setProperty('--text', '#fff'); | |
| root.style.setProperty('--card-bg', 'rgba(255,255,255,0.05)'); | |
| root.style.setProperty('--text-dim', 'rgba(255,255,255,0.6)'); | |
| } | |
| } | |
| function toggleSidebar() { | |
| document.getElementById('sidebar').classList.toggle('show'); | |
| } | |
| function openEditor() { | |
| document.getElementById('editorOverlay').style.display = 'flex'; | |
| if (document.getElementById('blocksContainer').children.length === 0) { | |
| addBlock('paragraph'); | |
| } | |
| } | |
| function addBlock(type, parentId = null) { | |
| const container = parentId ? document.getElementById(parentId + '-inner') : document.getElementById('blocksContainer'); | |
| const blockId = 'block-' + Math.random().toString(36).substr(2, 9); | |
| const block = document.createElement('div'); | |
| block.className = `block-item ${type}-block`; | |
| block.id = blockId; | |
| block.dataset.type = type; | |
| let inputHtml = `<div class="block-label">${type}</div>`; | |
| if (type === 'section') { | |
| inputHtml += `<div id="${blockId}-inner" class="container-block"></div> | |
| <button class="add-block-btn" style="width:100%; margin-top:10px" onclick="addBlock('paragraph', '${blockId}')">+ Add inside Section</button>`; | |
| } else if (type === 'columns') { | |
| inputHtml += `<div id="${blockId}-inner" class="columns-block"> | |
| <div class="col-inner" id="${blockId}-col1" style="border:1px dashed rgba(255,255,255,0.1); padding:10px; border-radius:8px"></div> | |
| <div class="col-inner" id="${blockId}-col2" style="border:1px dashed rgba(255,255,255,0.1); padding:10px; border-radius:8px"></div> | |
| </div> | |
| <div style="display:flex; gap:8px; margin-top:10px"> | |
| <button class="add-block-btn" style="flex:1" onclick="addBlock('paragraph', '${blockId}-col1')">+ Left</button> | |
| <button class="add-block-btn" style="flex:1" onclick="addBlock('paragraph', '${blockId}-col2')">+ Right</button> | |
| </div>`; | |
| } else if (type === 'header') { | |
| inputHtml += `<input type="text" class="block-input block-header-input" placeholder="Enter heading...">`; | |
| } else if (type === 'paragraph') { | |
| inputHtml += `<textarea class="block-input" placeholder="Start typing..." oninput="autoResize(this)"></textarea>`; | |
| } else if (type === 'spacer') { | |
| inputHtml += `<div class="spacer-editor">Vertical Spacer (40px)</div>`; | |
| } else if (type === 'hero') { | |
| inputHtml += `<div class="hero-editor"> | |
| <input type="text" class="block-input block-header-input" style="text-align:center" placeholder="Hero Title"> | |
| <textarea class="block-input" style="text-align:center" placeholder="Hero description..."></textarea> | |
| <button class="block-button-preview" style="margin-top:10px">Get Started</button> | |
| </div>`; | |
| } else if (type === 'navbar') { | |
| inputHtml += `<div class="navbar-editor"> | |
| <div style="font-weight:bold">LOGO</div> | |
| <div style="display:flex; gap:10px; font-size:12px">Home | About | Contact</div> | |
| </div>`; | |
| } else if (type === 'footer') { | |
| inputHtml += `<div style="text-align:center; padding:20px; font-size:12px; color:var(--text-dim)"> | |
| © 2026 FynWrite | Built with Love | |
| </div>`; | |
| } else if (type === 'button') { | |
| inputHtml += `<div class="block-button-editor"> | |
| <input type="text" class="form-control" style="margin:0; flex:1" placeholder="Text"> | |
| <input type="text" class="form-control" style="margin:0; flex:2" placeholder="URL"> | |
| <div class="block-button-preview">Button</div> | |
| </div>`; | |
| } else if (type === 'html') { | |
| inputHtml += `<textarea class="block-input block-html-input" placeholder="<!-- Custom HTML -->" oninput="autoResize(this)"></textarea>`; | |
| } | |
| block.innerHTML = ` | |
| <div class="block-controls"> | |
| <button class="icon-btn" onclick="moveBlock('${blockId}', -1)">↑</button> | |
| <button class="icon-btn" onclick="moveBlock('${blockId}', 1)">↓</button> | |
| <button class="icon-btn" onclick="removeBlock('${blockId}')">🗑️</button> | |
| </div> | |
| ${inputHtml} | |
| `; | |
| container.appendChild(block); | |
| if (parentId) event.stopPropagation(); | |
| } | |
| function autoResize(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = textarea.scrollHeight + 'px'; | |
| } | |
| function removeBlock(id) { | |
| document.getElementById(id).remove(); | |
| } | |
| function moveBlock(id, direction) { | |
| const block = document.getElementById(id); | |
| const container = document.getElementById('blocksContainer'); | |
| if (direction === -1 && block.previousElementSibling) { | |
| container.insertBefore(block, block.previousElementSibling); | |
| } else if (direction === 1 && block.nextElementSibling) { | |
| container.insertBefore(block.nextElementSibling, block); | |
| } | |
| } | |
| function updateButtonPreview(id) { | |
| const block = document.getElementById(id); | |
| const inputs = block.querySelectorAll('input'); | |
| const preview = block.querySelector('.block-button-preview'); | |
| preview.innerText = inputs[0].value || 'Button'; | |
| } | |
| async function publishPost() { | |
| const title = document.getElementById('postTitle').value; | |
| if (!title) return alert('Title is required'); | |
| // Gather block data | |
| const blocks = Array.from(document.getElementById('blocksContainer').children).map(block => { | |
| const type = block.dataset.type; | |
| let content = ''; | |
| let metadata = {}; | |
| const input = block.querySelector('.block-input'); | |
| if (input) content = input.value; | |
| if (type === 'button') { | |
| const inputs = block.querySelectorAll('input'); | |
| metadata = { text: inputs[0].value, url: inputs[1].value }; | |
| } | |
| return { type, content, metadata }; | |
| }); | |
| try { | |
| const res = await auth.fetchWithAuth('/api/fynwrite/posts', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| title: title, | |
| content: JSON.stringify(blocks), | |
| is_published: true | |
| }) | |
| }); | |
| if (!res.ok) { | |
| const errorData = await res.json().catch(() => ({})); | |
| throw new Error(errorData.error || 'Failed to publish'); | |
| } | |
| alert('Post published!'); | |
| document.getElementById('postTitle').value = ''; | |
| document.getElementById('blocksContainer').innerHTML = ''; | |
| document.getElementById('editorOverlay').style.display = 'none'; | |
| await loadPosts(); | |
| showView('posts'); | |
| } catch (e) { | |
| alert('Error: ' + e.message); | |
| } | |
| } | |
| async function saveDraft() { | |
| const title = document.getElementById('postTitle').value; | |
| if (!title) { | |
| closeEditor(); | |
| return; | |
| } | |
| // Gather block data | |
| const blocks = Array.from(document.getElementById('blocksContainer').children).map(block => { | |
| const type = block.dataset.type; | |
| let content = ''; | |
| let metadata = {}; | |
| const input = block.querySelector('.block-input'); | |
| if (input) content = input.value; | |
| if (type === 'button') { | |
| const inputs = block.querySelectorAll('input'); | |
| metadata = { text: inputs[0].value, url: inputs[1].value }; | |
| } | |
| return { type, content, metadata }; | |
| }); | |
| try { | |
| const res = await auth.fetchWithAuth('/api/fynwrite/posts', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| title: title, | |
| content: JSON.stringify(blocks), | |
| is_published: false | |
| }) | |
| }); | |
| if (res.ok) { | |
| await loadPosts(); | |
| } | |
| } catch (e) { | |
| console.error('Error saving draft:', e); | |
| } finally { | |
| closeEditor(); | |
| } | |
| } | |
| function closeEditor() { | |
| document.getElementById('postTitle').value = ''; | |
| document.getElementById('blocksContainer').innerHTML = ''; | |
| document.getElementById('editorOverlay').style.display = 'none'; | |
| } | |
| let currentFilter = 'all'; | |
| let posts = []; | |
| function filterPosts(status) { | |
| currentFilter = status; | |
| document.querySelectorAll('.filter-chip').forEach(chip => { | |
| const text = chip.innerText.toLowerCase(); | |
| chip.classList.toggle('active', text.includes(status)); | |
| }); | |
| renderPosts(); | |
| } | |
| function renderPosts() { | |
| const container = document.getElementById('postsList'); | |
| if (!container) return; | |
| const filtered = (typeof posts !== 'undefined' ? posts : []).filter(p => { | |
| const isRemoved = p.is_removed === true || p.is_removed === 'true' || p.status === 'trash'; | |
| const isPublished = p.is_published === true || p.is_published === 'true' || p.status === 'published'; | |
| if (currentFilter === 'all') return !isRemoved; | |
| if (currentFilter === 'published') return isPublished && !isRemoved; | |
| if (currentFilter === 'draft') return !isPublished && !isRemoved; | |
| if (currentFilter === 'trash') return isRemoved; | |
| return true; | |
| }); | |
| // Update counts | |
| const allCount = posts.filter(p => !(p.is_removed || p.status === 'trash')).length; | |
| const pubCount = posts.filter(p => (p.is_published === true || p.status === 'published' || p.is_published === 'true') && !(p.is_removed || p.status === 'trash')).length; | |
| const draftCount = posts.filter(p => (p.is_published === false || p.status === 'draft' || p.is_published === 'false') && !(p.is_removed || p.status === 'trash')).length; | |
| const trashCount = posts.filter(p => (p.is_removed || p.status === 'trash')).length; | |
| if (document.getElementById('countAll')) document.getElementById('countAll').innerText = `(${allCount})`; | |
| if (document.getElementById('countPublished')) document.getElementById('countPublished').innerText = `(${pubCount})`; | |
| if (document.getElementById('countDraft')) document.getElementById('countDraft').innerText = `(${draftCount})`; | |
| if (document.getElementById('countTrash')) document.getElementById('countTrash').innerText = `(${trashCount})`; | |
| if (filtered.length === 0) { | |
| container.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-dim)">No posts found</div>'; | |
| return; | |
| } | |
| container.innerHTML = filtered.map(post => ` | |
| <div class="post-card"> | |
| <div class="post-card-thumb"> | |
| ${(post.coverImage || post.cover_image) ? `<img src="${post.coverImage || post.cover_image}" alt="">` : `<div style="width:100%;height:100%;background:#f0f0f0;display:flex;align-items:center;justify-content:center;color:#ccc;font-size:24px;">📝</div>`} | |
| <div class="post-badge ${(post.is_published === true || post.is_published === 'true') ? 'badge-published' : 'badge-draft'}"> | |
| ${(post.is_published === true || post.is_published === 'true') ? 'Published' : 'Draft'} | |
| </div> | |
| </div> | |
| <div class="post-card-content"> | |
| <button class="post-options-btn" onclick="togglePostOptions(event, '${post.slug}')">⋮</button> | |
| <div class="post-card-title">${post.title}</div> | |
| <div class="post-card-meta"> | |
| ${(post.is_published === true || post.is_published === 'true') ? 'Published' : 'Draft'} • ${new Date(post.createdAt || post.created_at || Date.now()).toLocaleDateString(undefined, {month:'short', day:'numeric'})} | |
| </div> | |
| <div class="post-card-stats"> | |
| <div class="stat-item">👁️ ${post.viewsCount || post.views_count || 0}</div> | |
| <div class="stat-item">💬 ${post.commentsCount || post.comments_count || 0}</div> | |
| <div class="stat-item">❤️ ${post.likesCount || post.likes_count || 0}</div> | |
| </div> | |
| <div id="options-${post.slug}" class="dropdown-menu"> | |
| <div class="dropdown-item" onclick="openEditor('${post.slug}')">✏️ Edit</div> | |
| <div class="dropdown-item" onclick="togglePublish('${post.slug}', ${!(post.is_published === true || post.is_published === 'true')})">${(post.is_published === true || post.is_published === 'true') ? '📁 Move to Draft' : '🚀 Publish'}</div> | |
| <div class="dropdown-item" onclick="deletePost('${post.slug}')" style="color:#f44336">🗑️ Delete</div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function togglePostOptions(event, slug) { | |
| event.stopPropagation(); | |
| const menu = document.getElementById(`options-${slug}`); | |
| const allMenus = document.querySelectorAll('.dropdown-menu'); | |
| allMenus.forEach(m => { if (m.id !== `options-${slug}`) m.style.display = 'none'; }); | |
| menu.style.display = menu.style.display === 'flex' ? 'none' : 'flex'; | |
| if (menu.style.display === 'flex') { | |
| const closeHandler = (e) => { | |
| if (!menu.contains(e.target)) { | |
| menu.style.display = 'none'; | |
| document.removeEventListener('click', closeHandler); | |
| } | |
| }; | |
| setTimeout(() => document.addEventListener('click', closeHandler), 1); | |
| } | |
| } | |
| async function togglePublish(slug, status) { | |
| try { | |
| const res = await auth.fetchWithAuth(`/api/fynwrite/posts/${slug}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ is_published: status }) | |
| }); | |
| if (res.ok) { | |
| loadPosts(); | |
| } | |
| } catch (err) { console.error(err); } | |
| } | |
| async function loadPosts() { | |
| try { | |
| const res = await auth.fetchWithAuth('/api/fynwrite/me/posts'); | |
| if (!res.ok) throw new Error('HTTP ' + res.status); | |
| const data = await res.json(); | |
| console.log('FynWrite posts loaded:', data); | |
| // Ensure data is treated as an array regardless of response structure | |
| posts = Array.isArray(data) ? data : (data && data.posts ? data.posts : []); | |
| renderPosts(); | |
| } catch (err) { | |
| console.error('Error loading FynWrite posts:', err); | |
| posts = []; | |
| renderPosts(); | |
| } | |
| } | |
| async function publishPost() { | |
| // Deprecated - use createPost | |
| return createPost(); | |
| } | |
| async function createPost() { | |
| const title = document.getElementById('postTitle').value; | |
| if (!title) return alert('Title is required'); | |
| // Gather block data | |
| const blocks = Array.from(document.getElementById('blocksContainer').children).map(block => { | |
| const type = block.dataset.type; | |
| let content = ''; | |
| let metadata = {}; | |
| const input = block.querySelector('.block-input'); | |
| if (input) content = input.value; | |
| if (type === 'button') { | |
| const inputs = block.querySelectorAll('input'); | |
| metadata = { text: inputs[0].value, url: inputs[1].value }; | |
| } | |
| return { type, content, metadata }; | |
| }); | |
| try { | |
| const res = await auth.fetchWithAuth('/api/fynwrite/posts', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| title: title, | |
| content: JSON.stringify(blocks), | |
| is_published: true | |
| }) | |
| }); | |
| if (!res.ok) { | |
| const errorData = await res.json().catch(() => ({})); | |
| throw new Error(errorData.error || 'Failed to publish'); | |
| } | |
| alert('Post published!'); | |
| closeEditor(); | |
| await loadPosts(); | |
| showView('posts'); | |
| } catch (e) { | |
| alert('Error: ' + e.message); | |
| } | |
| } | |
| async function deletePost(slug) { | |
| if (!confirm('Are you sure you want to delete this post?')) return; | |
| try { | |
| const res = await auth.fetchWithAuth(`/api/fynwrite/posts/${slug}`, { | |
| method: 'DELETE' | |
| }); | |
| if (res.ok) { | |
| loadPosts(); | |
| } | |
| } catch (err) { console.error(err); } | |
| } | |
| </script> | |
| </body> | |
| </html> | |