Mobile-emulator / fynwrite.html
Akira
Initial commit without large assets
4e5205d
<!DOCTYPE html>
<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>