← back to Designer Wallcoverings
DW-Agents/dw-agents/dw-blog-agent/public/index.html
2203 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DW Blog Agent - Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding-top: 120px;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #0f766e, #0284c7, #7c3aed);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
font-size: 24px;
font-weight: 700;
}
.header-icon {
font-size: 32px;
}
.logout-btn {
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.3);
color: white;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.logout-btn:hover {
background: rgba(255,255,255,0.3);
}
.tab-nav {
position: fixed;
top: 72px;
left: 0;
right: 0;
background: white;
display: flex;
gap: 5px;
padding: 12px 30px;
border-bottom: 2px solid #e5e5e5;
z-index: 999;
overflow-x: auto;
}
.tab-btn {
background: transparent;
border: none;
padding: 12px 24px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
color: #666;
border-radius: 8px;
transition: all 0.3s;
white-space: nowrap;
}
.tab-btn:hover {
background: #f0f0f0;
color: #333;
}
.tab-btn.active {
background: linear-gradient(135deg, #0f766e, #0284c7);
color: white;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 30px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-label {
font-size: 14px;
color: #666;
font-weight: 600;
}
.stat-value {
font-size: 32px;
font-weight: 700;
}
.stat-icon {
font-size: 24px;
margin-bottom: 8px;
}
.stat-card.total .stat-value { color: #0284c7; }
.stat-card.published .stat-value { color: #2ecc71; }
.stat-card.scheduled .stat-value { color: #833ab4; }
.stat-card.drafts .stat-value { color: #999; }
.stat-card.failed .stat-value { color: #e74c3c; }
.section-header {
font-size: 22px;
font-weight: 700;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.post-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
overflow: hidden;
transition: transform 0.3s;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
}
.post-image {
width: 100%;
height: 200px;
object-fit: cover;
background: #e5e5e5;
}
.post-content {
padding: 16px;
}
.post-title {
font-size: 16px;
font-weight: 700;
color: #333;
margin-bottom: 8px;
line-height: 1.4;
}
.post-meta {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.post-vendor {
font-size: 13px;
color: #666;
}
.post-date {
font-size: 12px;
color: #999;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.published { background: #2ecc71; color: white; }
.status-badge.scheduled { background: #833ab4; color: white; }
.status-badge.draft { background: #999; color: white; }
.status-badge.failed { background: #e74c3c; color: white; }
.post-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #0f766e, #0284c7);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-success {
background: #2ecc71;
color: white;
}
.btn-success:hover {
background: #27ae60;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-secondary {
background: #f0f0f0;
color: #333;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.calendar-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 16px;
}
.calendar-nav {
display: flex;
gap: 12px;
align-items: center;
}
.calendar-title {
font-size: 20px;
font-weight: 700;
color: #333;
}
.view-toggle {
display: flex;
gap: 8px;
}
.view-btn {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.view-btn.active {
background: linear-gradient(135deg, #0f766e, #0284c7);
color: white;
border-color: transparent;
}
.calendar-month {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.calendar-header {
font-size: 13px;
font-weight: 700;
color: #666;
text-align: center;
padding: 12px;
}
.calendar-day {
min-height: 120px;
border: 2px solid #e5e5e5;
border-radius: 8px;
padding: 8px;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.calendar-day:hover {
border-color: #0284c7;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.calendar-day.today {
border-color: #fbbf24;
box-shadow: 0 0 0 2px #fef3c7;
}
.calendar-day.has-published {
border-left: 4px solid #2ecc71;
}
.calendar-day.has-scheduled {
border-left: 4px solid #833ab4;
}
.calendar-day.outside-month {
background: #f9f9f9;
color: #ccc;
}
.calendar-day-number {
font-size: 14px;
font-weight: 700;
color: #333;
margin-bottom: 8px;
}
.calendar-day.outside-month .calendar-day-number {
color: #ccc;
}
.calendar-posts {
display: flex;
flex-direction: column;
gap: 4px;
}
.calendar-post-thumb {
width: 100%;
height: 40px;
border-radius: 4px;
object-fit: cover;
cursor: grab;
transition: opacity 0.3s;
}
.calendar-post-thumb.dragging {
opacity: 0.5;
}
.calendar-day.drag-over {
border: 2px dashed #0284c7;
background: #eff6ff;
}
.calendar-more {
font-size: 11px;
color: #666;
text-align: center;
margin-top: 4px;
font-weight: 600;
}
.calendar-week {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
}
.calendar-week-day {
background: white;
border-radius: 8px;
padding: 12px;
min-height: 400px;
overflow-y: auto;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.week-day-header {
font-size: 14px;
font-weight: 700;
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #e5e5e5;
}
.week-post-card {
background: #f9f9f9;
border-radius: 8px;
padding: 8px;
margin-bottom: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.week-post-image {
width: 100%;
height: 60px;
border-radius: 4px;
object-fit: cover;
}
.week-post-title {
font-size: 12px;
font-weight: 600;
color: #333;
}
.calendar-today-view {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.today-post-card {
background: #f9f9f9;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
display: flex;
gap: 16px;
}
.today-post-image {
width: 150px;
height: 150px;
border-radius: 8px;
object-fit: cover;
}
.today-post-content {
flex: 1;
}
.today-post-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
}
.today-post-meta {
display: flex;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.filter-bar {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.filter-bar select,
.filter-bar input {
padding: 10px 16px;
border: 2px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
.filter-bar select {
min-width: 150px;
}
.filter-bar input[type="text"] {
flex: 1;
min-width: 200px;
}
.posts-table {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.table-row {
display: grid;
grid-template-columns: 80px 1fr 150px 100px 120px 150px 200px;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e5e5e5;
align-items: center;
}
.table-row:last-child {
border-bottom: none;
}
.table-header {
background: #f9f9f9;
font-weight: 700;
color: #666;
font-size: 13px;
}
.table-thumb {
width: 60px;
height: 60px;
border-radius: 6px;
object-fit: cover;
}
.load-more-container {
text-align: center;
margin-top: 30px;
}
.vendor-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.vendor-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.3s;
}
.vendor-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
}
.vendor-name {
font-size: 20px;
font-weight: 700;
color: #333;
margin-bottom: 16px;
}
.vendor-counts {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.vendor-count-item {
display: flex;
justify-content: space-between;
font-size: 14px;
}
.vendor-count-label {
color: #666;
}
.vendor-count-value {
font-weight: 700;
}
.vendor-bar {
height: 8px;
background: #e5e5e5;
border-radius: 4px;
overflow: hidden;
display: flex;
}
.vendor-bar-segment {
height: 100%;
}
.vendor-bar-segment.published { background: #2ecc71; }
.vendor-bar-segment.scheduled { background: #833ab4; }
.vendor-bar-segment.draft { background: #999; }
.new-post-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.form-column {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e5e5;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.3s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: #0284c7;
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-textarea.large {
min-height: 400px;
}
.image-preview {
width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 8px;
margin-top: 12px;
}
.char-counter {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.char-counter.over {
color: #e74c3c;
font-weight: 700;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
.preview-container {
background: #f9f9f9;
border: 2px solid #e5e5e5;
border-radius: 8px;
padding: 16px;
margin-top: 12px;
max-height: 300px;
overflow-y: auto;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 2000;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 16px;
max-width: 700px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.modal-header {
padding: 24px;
border-bottom: 2px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 20px;
font-weight: 700;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.modal-close:hover {
background: #f0f0f0;
}
.modal-body {
padding: 24px;
}
.instagram-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.instagram-item {
display: flex;
gap: 16px;
padding: 12px;
border: 2px solid #e5e5e5;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.instagram-item:hover {
border-color: #0284c7;
background: #f9f9f9;
}
.instagram-thumb {
width: 80px;
height: 80px;
border-radius: 6px;
object-fit: cover;
}
.instagram-info {
flex: 1;
}
.instagram-title {
font-size: 15px;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.instagram-vendor {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.instagram-date {
font-size: 12px;
color: #999;
}
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 3000;
display: flex;
flex-direction: column;
gap: 12px;
}
.toast {
background: white;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
min-width: 300px;
display: flex;
align-items: center;
gap: 12px;
border-left: 4px solid #333;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast.success {
border-left-color: #2ecc71;
}
.toast.error {
border-left-color: #e74c3c;
}
.toast.info {
border-left-color: #0284c7;
}
.toast-icon {
font-size: 20px;
}
.toast-message {
flex: 1;
font-size: 14px;
color: #333;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-state-text {
font-size: 16px;
font-weight: 600;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
font-size: 14px;
}
@media (max-width: 768px) {
body {
padding-top: 140px;
}
.header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.header-left {
justify-content: center;
}
.logout-btn {
width: 100%;
}
.tab-nav {
top: 110px;
}
.container {
padding: 20px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.posts-grid {
grid-template-columns: 1fr;
}
.calendar-month {
gap: 5px;
padding: 10px;
}
.calendar-day {
min-height: 80px;
padding: 4px;
}
.calendar-day-number {
font-size: 12px;
}
.calendar-post-thumb {
height: 30px;
}
.calendar-week {
grid-template-columns: 1fr;
}
.table-row {
grid-template-columns: 1fr;
gap: 8px;
}
.new-post-form {
grid-template-columns: 1fr;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-bar select,
.filter-bar input {
width: 100%;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<span class="header-icon">📝</span>
<span>DW Blog Agent</span>
</div>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
<div class="tab-nav">
<button class="tab-btn active" onclick="switchTab('dashboard')">Dashboard</button>
<button class="tab-btn" onclick="switchTab('calendar')">Calendar</button>
<button class="tab-btn" onclick="switchTab('posts')">Posts</button>
<button class="tab-btn" onclick="switchTab('vendors')">By Vendor</button>
<button class="tab-btn" onclick="switchTab('new-post')">New Post</button>
</div>
<div class="container">
<!-- Dashboard Tab -->
<div id="dashboard-tab" class="tab-content active">
<div class="stats-grid" id="stats-grid">
<div class="stat-card total">
<div class="stat-icon">📊</div>
<div class="stat-label">Total Posts</div>
<div class="stat-value" id="stat-total">0</div>
</div>
<div class="stat-card published">
<div class="stat-icon">✅</div>
<div class="stat-label">Published</div>
<div class="stat-value" id="stat-published">0</div>
</div>
<div class="stat-card scheduled">
<div class="stat-icon">📅</div>
<div class="stat-label">Scheduled</div>
<div class="stat-value" id="stat-scheduled">0</div>
</div>
<div class="stat-card drafts">
<div class="stat-icon">📝</div>
<div class="stat-label">Drafts</div>
<div class="stat-value" id="stat-drafts">0</div>
</div>
<div class="stat-card failed">
<div class="stat-icon">❌</div>
<div class="stat-label">Failed</div>
<div class="stat-value" id="stat-failed">0</div>
</div>
</div>
<div class="section-header">
<span>📰</span>
<span>Recent Posts</span>
</div>
<div class="posts-grid" id="recent-posts-grid">
<div class="loading">Loading recent posts...</div>
</div>
</div>
<!-- Calendar Tab -->
<div id="calendar-tab" class="tab-content">
<div class="calendar-controls">
<div class="calendar-nav">
<button class="btn btn-secondary" onclick="navigateCalendar(-1)">← Prev</button>
<button class="btn btn-secondary" onclick="navigateCalendar(0)">Today</button>
<button class="btn btn-secondary" onclick="navigateCalendar(1)">Next →</button>
<div class="calendar-title" id="calendar-title">Loading...</div>
</div>
<div class="view-toggle">
<button class="view-btn active" onclick="changeCalendarView('month')">Month</button>
<button class="view-btn" onclick="changeCalendarView('week')">Week</button>
<button class="view-btn" onclick="changeCalendarView('today')">Today</button>
</div>
</div>
<div id="calendar-month-view" class="calendar-month">
<div class="loading">Loading calendar...</div>
</div>
<div id="calendar-week-view" class="calendar-week" style="display: none;">
<div class="loading">Loading week...</div>
</div>
<div id="calendar-today-view" class="calendar-today-view" style="display: none;">
<div class="loading">Loading today...</div>
</div>
</div>
<!-- Posts Tab -->
<div id="posts-tab" class="tab-content">
<div class="filter-bar">
<select id="filter-vendor" class="form-select" onchange="loadPosts()">
<option value="">All Vendors</option>
</select>
<select id="filter-status" class="form-select" onchange="loadPosts()">
<option value="">All Statuses</option>
<option value="draft">Draft</option>
<option value="scheduled">Scheduled</option>
<option value="published">Published</option>
<option value="failed">Failed</option>
</select>
<input type="text" id="filter-search" class="form-input" placeholder="Search by title, vendor, or SKU..." oninput="filterPostsLocal()">
<button class="btn btn-primary" onclick="loadPosts()">🔄 Refresh</button>
</div>
<div class="posts-table">
<div class="table-row table-header">
<div>Image</div>
<div>Title</div>
<div>Vendor</div>
<div>Status</div>
<div>DW SKU</div>
<div>Date</div>
<div>Actions</div>
</div>
<div id="posts-table-body">
<div class="loading">Loading posts...</div>
</div>
</div>
<div class="load-more-container">
<button class="btn btn-secondary" id="load-more-btn" onclick="loadMorePosts()" style="display: none;">Load More</button>
</div>
</div>
<!-- Vendors Tab -->
<div id="vendors-tab" class="tab-content">
<div class="section-header">
<span>🏢</span>
<span>Posts by Vendor</span>
</div>
<div class="vendor-grid" id="vendor-grid">
<div class="loading">Loading vendors...</div>
</div>
</div>
<!-- New Post Tab -->
<div id="new-post-tab" class="tab-content">
<div class="section-header">
<span>✨</span>
<span id="new-post-header">Create New Post</span>
</div>
<div class="new-post-form">
<div class="form-column">
<h3 style="margin-bottom: 20px;">Product Details</h3>
<input type="hidden" id="edit-post-id">
<div class="form-group">
<label class="form-label">Title *</label>
<input type="text" id="post-title" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Vendor</label>
<input type="text" id="post-vendor" class="form-input">
</div>
<div class="form-group">
<label class="form-label">DW SKU</label>
<input type="text" id="post-sku" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Product Handle</label>
<input type="text" id="post-handle" class="form-input" placeholder="for designerwallcoverings.com link">
</div>
<div class="form-group">
<label class="form-label">Image URL</label>
<input type="text" id="post-image" class="form-input" oninput="previewImage()">
<img id="image-preview" class="image-preview" style="display: none;" onerror="this.style.display='none'">
</div>
<div class="form-group">
<label class="form-label">Tags (comma-separated)</label>
<input type="text" id="post-tags" class="form-input" placeholder="wallpaper, luxury, floral">
</div>
<div class="form-group">
<label class="form-label">Schedule Date/Time</label>
<input type="datetime-local" id="post-schedule" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select id="post-status" class="form-select">
<option value="draft">Draft</option>
<option value="scheduled">Scheduled</option>
<option value="published">Publish Now</option>
</select>
</div>
<div class="form-actions">
<button class="btn btn-primary" onclick="generateContent()">🤖 Generate Content</button>
<button class="btn btn-success" onclick="savePost()">💾 Save Post</button>
<button class="btn btn-secondary" onclick="openInstagramImport()">📸 Import from Instagram</button>
</div>
</div>
<div class="form-column">
<h3 style="margin-bottom: 20px;">Content Editor</h3>
<div class="form-group">
<label class="form-label">SEO Title</label>
<input type="text" id="seo-title" class="form-input" maxlength="70" oninput="updateCharCounter('seo-title', 70)">
<div class="char-counter" id="seo-title-counter">0 / 70 characters</div>
</div>
<div class="form-group">
<label class="form-label">SEO Description</label>
<textarea id="seo-description" class="form-textarea" maxlength="160" oninput="updateCharCounter('seo-description', 160)"></textarea>
<div class="char-counter" id="seo-description-counter">0 / 160 characters</div>
</div>
<div class="form-group">
<label class="form-label">Body HTML</label>
<textarea id="post-body" class="form-textarea large"></textarea>
</div>
<button class="btn btn-secondary" onclick="togglePreview()">👁️ Toggle Preview</button>
<div id="preview-container" class="preview-container" style="display: none;"></div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="modal-title">Modal</div>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-body" id="modal-body">
<!-- Dynamic content -->
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<script>
// Global state
let currentTab = 'dashboard';
let currentCalendarView = 'month';
let currentCalendarDate = new Date();
let postsOffset = 0;
let postsLimit = 20;
let allPostsData = [];
let hasMorePosts = true;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadStats();
loadRecentPosts();
loadVendors();
loadVendorsDropdown();
// Auto-refresh stats every 30 seconds
setInterval(loadStats, 30000);
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
// Close modal on overlay click
document.getElementById('modal').addEventListener('click', function(e) {
if (e.target.id === 'modal') {
closeModal();
}
});
});
// Navigation
function switchTab(tab) {
currentTab = tab;
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tab + '-tab').classList.add('active');
// Load data for specific tabs
if (tab === 'calendar') {
loadCalendar();
} else if (tab === 'posts') {
postsOffset = 0;
hasMorePosts = true;
loadPosts();
} else if (tab === 'vendors') {
loadVendors();
} else if (tab === 'new-post') {
resetNewPostForm();
}
}
function logout() {
window.location.href = '/logout';
}
// Stats
async function loadStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
document.getElementById('stat-total').textContent = data.total || 0;
document.getElementById('stat-published').textContent = data.published || 0;
document.getElementById('stat-scheduled').textContent = data.scheduled || 0;
document.getElementById('stat-drafts').textContent = data.drafts || 0;
document.getElementById('stat-failed').textContent = data.failed || 0;
} catch (error) {
console.error('Error loading stats:', error);
}
}
// Dashboard - Recent Posts
async function loadRecentPosts() {
try {
const response = await fetch('/api/posts?limit=12');
const data = await response.json();
const grid = document.getElementById('recent-posts-grid');
if (!data.posts || data.posts.length === 0) {
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📭</div><div class="empty-state-text">No posts yet</div></div>';
return;
}
grid.innerHTML = data.posts.map(post => createPostCard(post)).join('');
} catch (error) {
console.error('Error loading recent posts:', error);
document.getElementById('recent-posts-grid').innerHTML = '<div class="empty-state"><div class="empty-state-icon">❌</div><div class="empty-state-text">Error loading posts</div></div>';
}
}
function createPostCard(post) {
const date = post.published_at || post.scheduled_at || post.created_at;
const formattedDate = date ? formatDate(date) : 'No date';
return `
<div class="post-card">
<img src="${escapeHtml(post.image_url || '')}" class="post-image" onerror="this.style.display='none'">
<div class="post-content">
<div class="post-title">${escapeHtml(post.title || 'Untitled')}</div>
<div class="post-meta">
<div class="post-vendor">${escapeHtml(post.vendor || 'No vendor')}</div>
<div><span class="status-badge ${post.status}">${post.status || 'draft'}</span></div>
<div class="post-date">${formattedDate}</div>
</div>
<div class="post-actions">
<button class="btn btn-secondary" onclick="editPost('${post.id}')">Edit</button>
${post.status !== 'published' ? `<button class="btn btn-success" onclick="publishPost('${post.id}')">Publish</button>` : ''}
<button class="btn btn-danger" onclick="deletePost('${post.id}')">Delete</button>
</div>
</div>
</div>
`;
}
// Calendar
async function loadCalendar() {
const view = currentCalendarView;
if (view === 'month') {
await renderMonthView();
} else if (view === 'week') {
await renderWeekView();
} else if (view === 'today') {
await renderTodayView();
}
}
function changeCalendarView(view) {
currentCalendarView = view;
// Update buttons
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Show/hide views
document.getElementById('calendar-month-view').style.display = view === 'month' ? 'grid' : 'none';
document.getElementById('calendar-week-view').style.display = view === 'week' ? 'grid' : 'none';
document.getElementById('calendar-today-view').style.display = view === 'today' ? 'block' : 'none';
loadCalendar();
}
function navigateCalendar(direction) {
if (direction === 0) {
currentCalendarDate = new Date();
} else if (currentCalendarView === 'month') {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + direction);
} else if (currentCalendarView === 'week') {
currentCalendarDate.setDate(currentCalendarDate.getDate() + (direction * 7));
}
loadCalendar();
}
async function renderMonthView() {
const year = currentCalendarDate.getFullYear();
const month = currentCalendarDate.getMonth();
// Update title
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
document.getElementById('calendar-title').textContent = `${monthNames[month]} ${year}`;
// Get first and last day of month
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Get start date (previous month days to fill grid)
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
// Get end date (next month days to fill grid)
const endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()));
// Fetch calendar data
const startStr = formatDateForAPI(startDate);
const endStr = formatDateForAPI(endDate);
try {
const response = await fetch(`/api/calendar?start=${startStr}&end=${endStr}`);
const data = await response.json();
const dateMap = data.dates || {};
// Render calendar grid
const container = document.getElementById('calendar-month-view');
container.innerHTML = `
<div class="calendar-header">Sun</div>
<div class="calendar-header">Mon</div>
<div class="calendar-header">Tue</div>
<div class="calendar-header">Wed</div>
<div class="calendar-header">Thu</div>
<div class="calendar-header">Fri</div>
<div class="calendar-header">Sat</div>
`;
const currentDate = new Date(startDate);
const today = new Date();
today.setHours(0, 0, 0, 0);
while (currentDate <= endDate) {
const dateStr = formatDateForAPI(currentDate);
const posts = dateMap[dateStr] || [];
const isCurrentMonth = currentDate.getMonth() === month;
const isToday = currentDate.getTime() === today.getTime();
const hasPublished = posts.some(p => p.status === 'published');
const hasScheduled = posts.some(p => p.status === 'scheduled');
let dayClasses = 'calendar-day';
if (!isCurrentMonth) dayClasses += ' outside-month';
if (isToday) dayClasses += ' today';
if (hasPublished) dayClasses += ' has-published';
if (hasScheduled) dayClasses += ' has-scheduled';
const dayEl = document.createElement('div');
dayEl.className = dayClasses;
dayEl.setAttribute('data-date', dateStr);
dayEl.ondragover = handleDragOver;
dayEl.ondrop = handleDrop;
dayEl.onclick = () => showDayPosts(dateStr, posts);
let postsHTML = '';
const maxVisible = 3;
posts.slice(0, maxVisible).forEach(post => {
postsHTML += `<img src="${escapeHtml(post.image_url || '')}" class="calendar-post-thumb" draggable="true" ondragstart="handleDragStart(event, '${post.id}')" data-post-id="${post.id}" onerror="this.style.display='none'">`;
});
if (posts.length > maxVisible) {
postsHTML += `<div class="calendar-more">+${posts.length - maxVisible} more</div>`;
}
dayEl.innerHTML = `
<div class="calendar-day-number">${currentDate.getDate()}</div>
<div class="calendar-posts">${postsHTML}</div>
`;
container.appendChild(dayEl);
currentDate.setDate(currentDate.getDate() + 1);
}
} catch (error) {
console.error('Error loading calendar:', error);
document.getElementById('calendar-month-view').innerHTML = '<div class="loading">Error loading calendar</div>';
}
}
async function renderWeekView() {
const startOfWeek = new Date(currentCalendarDate);
startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(endOfWeek.getDate() + 6);
// Update title
document.getElementById('calendar-title').textContent = `Week of ${formatDate(startOfWeek)}`;
// Fetch data
const startStr = formatDateForAPI(startOfWeek);
const endStr = formatDateForAPI(endOfWeek);
try {
const response = await fetch(`/api/calendar?start=${startStr}&end=${endStr}`);
const data = await response.json();
const dateMap = data.dates || {};
const container = document.getElementById('calendar-week-view');
container.innerHTML = '';
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const currentDate = new Date(startOfWeek);
for (let i = 0; i < 7; i++) {
const dateStr = formatDateForAPI(currentDate);
const posts = dateMap[dateStr] || [];
const dayCol = document.createElement('div');
dayCol.className = 'calendar-week-day';
let postsHTML = '';
posts.forEach(post => {
postsHTML += `
<div class="week-post-card">
<img src="${escapeHtml(post.image_url || '')}" class="week-post-image" onerror="this.style.display='none'">
<div class="week-post-title">${escapeHtml(post.title || 'Untitled')}</div>
<div><span class="status-badge ${post.status}">${post.status}</span></div>
<div style="font-size: 11px; color: #999;">${escapeHtml(post.vendor || '')}</div>
</div>
`;
});
if (posts.length === 0) {
postsHTML = '<div class="empty-state"><div class="empty-state-icon" style="font-size: 32px;">📭</div><div class="empty-state-text" style="font-size: 12px;">No posts</div></div>';
}
dayCol.innerHTML = `
<div class="week-day-header">${dayNames[i]}<br>${currentDate.getDate()}</div>
${postsHTML}
`;
container.appendChild(dayCol);
currentDate.setDate(currentDate.getDate() + 1);
}
} catch (error) {
console.error('Error loading week view:', error);
document.getElementById('calendar-week-view').innerHTML = '<div class="loading">Error loading week</div>';
}
}
async function renderTodayView() {
const today = new Date();
const dateStr = formatDateForAPI(today);
document.getElementById('calendar-title').textContent = `Today - ${formatDate(today)}`;
try {
const response = await fetch(`/api/calendar?start=${dateStr}&end=${dateStr}`);
const data = await response.json();
const posts = data.dates[dateStr] || [];
const container = document.getElementById('calendar-today-view');
if (posts.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📅</div><div class="empty-state-text">No posts scheduled for today</div></div>';
return;
}
container.innerHTML = posts.map(post => `
<div class="today-post-card">
<img src="${escapeHtml(post.image_url || '')}" class="today-post-image" onerror="this.style.display='none'">
<div class="today-post-content">
<div class="today-post-title">${escapeHtml(post.title || 'Untitled')}</div>
<div class="today-post-meta">
<span class="status-badge ${post.status}">${post.status}</span>
<span>${escapeHtml(post.vendor || '')}</span>
${post.scheduled_at ? `<span>⏰ ${new Date(post.scheduled_at).toLocaleTimeString()}</span>` : ''}
</div>
<div style="font-size: 14px; color: #666; margin-top: 12px;">${escapeHtml((post.body_html || '').substring(0, 200))}...</div>
<div class="post-actions" style="margin-top: 16px;">
<button class="btn btn-secondary" onclick="editPost('${post.id}')">Edit</button>
${post.status !== 'published' ? `<button class="btn btn-success" onclick="publishPost('${post.id}')">Publish</button>` : ''}
<button class="btn btn-danger" onclick="deletePost('${post.id}')">Delete</button>
</div>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading today view:', error);
document.getElementById('calendar-today-view').innerHTML = '<div class="loading">Error loading today</div>';
}
}
function showDayPosts(dateStr, posts) {
if (posts.length === 0) return;
const date = new Date(dateStr);
const content = `
<div style="display: flex; flex-direction: column; gap: 12px;">
${posts.map(post => `
<div class="instagram-item" onclick="editPost('${post.id}')">
<img src="${escapeHtml(post.image_url || '')}" class="instagram-thumb" onerror="this.style.display='none'">
<div class="instagram-info">
<div class="instagram-title">${escapeHtml(post.title || 'Untitled')}</div>
<div class="instagram-vendor">${escapeHtml(post.vendor || 'No vendor')} • <span class="status-badge ${post.status}">${post.status}</span></div>
<div class="instagram-date">${escapeHtml(post.dw_sku || '')}</div>
</div>
</div>
`).join('')}
</div>
`;
showModal(`Posts on ${formatDate(date)}`, content);
}
// Drag and Drop
function handleDragStart(e, postId) {
e.dataTransfer.setData('postId', postId);
e.target.classList.add('dragging');
}
function handleDragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('drag-over');
}
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('drag-over');
const postId = e.dataTransfer.getData('postId');
const newDate = e.currentTarget.getAttribute('data-date');
if (!postId || !newDate) return;
reschedulePost(postId, newDate);
// Remove dragging class
document.querySelectorAll('.dragging').forEach(el => {
el.classList.remove('dragging');
});
}
async function reschedulePost(postId, newDate) {
try {
const response = await fetch(`/api/posts/${postId}/reschedule`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scheduled_date: newDate })
});
if (response.ok) {
showToast('Post rescheduled successfully', 'success');
loadCalendar();
} else {
showToast('Failed to reschedule post', 'error');
}
} catch (error) {
console.error('Error rescheduling post:', error);
showToast('Error rescheduling post', 'error');
}
}
// Posts Tab
async function loadPosts() {
const vendor = document.getElementById('filter-vendor').value;
const status = document.getElementById('filter-status').value;
let url = `/api/posts?limit=${postsLimit}&offset=${postsOffset}`;
if (vendor) url += `&vendor=${encodeURIComponent(vendor)}`;
if (status) url += `&status=${status}`;
try {
const response = await fetch(url);
const data = await response.json();
if (postsOffset === 0) {
allPostsData = data.posts || [];
} else {
allPostsData = [...allPostsData, ...(data.posts || [])];
}
hasMorePosts = data.has_more || false;
renderPostsTable();
document.getElementById('load-more-btn').style.display = hasMorePosts ? 'inline-block' : 'none';
} catch (error) {
console.error('Error loading posts:', error);
document.getElementById('posts-table-body').innerHTML = '<div class="loading">Error loading posts</div>';
}
}
function loadMorePosts() {
postsOffset += postsLimit;
loadPosts();
}
function renderPostsTable() {
const tbody = document.getElementById('posts-table-body');
const searchTerm = document.getElementById('filter-search').value.toLowerCase();
let filteredPosts = allPostsData;
if (searchTerm) {
filteredPosts = allPostsData.filter(post => {
return (post.title || '').toLowerCase().includes(searchTerm) ||
(post.vendor || '').toLowerCase().includes(searchTerm) ||
(post.dw_sku || '').toLowerCase().includes(searchTerm);
});
}
if (filteredPosts.length === 0) {
tbody.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📭</div><div class="empty-state-text">No posts found</div></div>';
return;
}
tbody.innerHTML = filteredPosts.map(post => {
const date = post.published_at || post.scheduled_at || post.created_at;
return `
<div class="table-row">
<div><img src="${escapeHtml(post.image_url || '')}" class="table-thumb" onerror="this.style.display='none'"></div>
<div>${escapeHtml(post.title || 'Untitled')}</div>
<div>${escapeHtml(post.vendor || '')}</div>
<div><span class="status-badge ${post.status}">${post.status}</span></div>
<div>${escapeHtml(post.dw_sku || '')}</div>
<div>${date ? formatDate(date) : ''}</div>
<div class="post-actions">
<button class="btn btn-secondary" onclick="editPost('${post.id}')">Edit</button>
${post.status !== 'published' ? `<button class="btn btn-success" onclick="publishPost('${post.id}')">Publish</button>` : ''}
<button class="btn btn-danger" onclick="deletePost('${post.id}')">Delete</button>
</div>
</div>
`;
}).join('');
}
function filterPostsLocal() {
renderPostsTable();
}
async function publishPost(id) {
if (!confirm('Are you sure you want to publish this post?')) return;
try {
const response = await fetch(`/api/posts/${id}/publish`, {
method: 'POST'
});
if (response.ok) {
showToast('Post published successfully', 'success');
loadStats();
if (currentTab === 'dashboard') loadRecentPosts();
if (currentTab === 'posts') loadPosts();
if (currentTab === 'calendar') loadCalendar();
} else {
showToast('Failed to publish post', 'error');
}
} catch (error) {
console.error('Error publishing post:', error);
showToast('Error publishing post', 'error');
}
}
async function deletePost(id) {
if (!confirm('Are you sure you want to delete this post? This cannot be undone.')) return;
try {
const response = await fetch(`/api/posts/${id}`, {
method: 'DELETE'
});
if (response.ok) {
showToast('Post deleted successfully', 'success');
loadStats();
if (currentTab === 'dashboard') loadRecentPosts();
if (currentTab === 'posts') loadPosts();
if (currentTab === 'calendar') loadCalendar();
} else {
showToast('Failed to delete post', 'error');
}
} catch (error) {
console.error('Error deleting post:', error);
showToast('Error deleting post', 'error');
}
}
async function editPost(id) {
try {
const response = await fetch(`/api/posts/${id}`);
const post = await response.json();
// Switch to new post tab
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-btn')[4].classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById('new-post-tab').classList.add('active');
// Populate form
document.getElementById('new-post-header').textContent = 'Edit Post';
document.getElementById('edit-post-id').value = post.id;
document.getElementById('post-title').value = post.title || '';
document.getElementById('post-vendor').value = post.vendor || '';
document.getElementById('post-sku').value = post.dw_sku || '';
document.getElementById('post-handle').value = post.product_handle || '';
document.getElementById('post-image').value = post.image_url || '';
document.getElementById('post-tags').value = post.tags || '';
document.getElementById('post-status').value = post.status || 'draft';
document.getElementById('seo-title').value = post.seo_title || '';
document.getElementById('seo-description').value = post.seo_description || '';
document.getElementById('post-body').value = post.body_html || '';
if (post.scheduled_at) {
const schedDate = new Date(post.scheduled_at);
const localDateTime = new Date(schedDate.getTime() - schedDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
document.getElementById('post-schedule').value = localDateTime;
}
previewImage();
updateCharCounter('seo-title', 70);
updateCharCounter('seo-description', 160);
} catch (error) {
console.error('Error loading post:', error);
showToast('Error loading post', 'error');
}
}
// Vendors Tab
async function loadVendors() {
try {
const response = await fetch('/api/vendors');
const data = await response.json();
const grid = document.getElementById('vendor-grid');
if (!data.vendors || data.vendors.length === 0) {
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏢</div><div class="empty-state-text">No vendors found</div></div>';
return;
}
grid.innerHTML = data.vendors.map(vendor => {
const total = vendor.published + vendor.scheduled + vendor.draft;
const publishedPct = total > 0 ? (vendor.published / total * 100) : 0;
const scheduledPct = total > 0 ? (vendor.scheduled / total * 100) : 0;
const draftPct = total > 0 ? (vendor.draft / total * 100) : 0;
return `
<div class="vendor-card" onclick="filterByVendor('${escapeHtml(vendor.name)}')">
<div class="vendor-name">${escapeHtml(vendor.name)}</div>
<div class="vendor-counts">
<div class="vendor-count-item">
<span class="vendor-count-label">Published</span>
<span class="vendor-count-value" style="color: #2ecc71;">${vendor.published}</span>
</div>
<div class="vendor-count-item">
<span class="vendor-count-label">Scheduled</span>
<span class="vendor-count-value" style="color: #833ab4;">${vendor.scheduled}</span>
</div>
<div class="vendor-count-item">
<span class="vendor-count-label">Drafts</span>
<span class="vendor-count-value" style="color: #999;">${vendor.draft}</span>
</div>
</div>
<div class="vendor-bar">
<div class="vendor-bar-segment published" style="width: ${publishedPct}%;"></div>
<div class="vendor-bar-segment scheduled" style="width: ${scheduledPct}%;"></div>
<div class="vendor-bar-segment draft" style="width: ${draftPct}%;"></div>
</div>
</div>
`;
}).join('');
} catch (error) {
console.error('Error loading vendors:', error);
document.getElementById('vendor-grid').innerHTML = '<div class="loading">Error loading vendors</div>';
}
}
async function loadVendorsDropdown() {
try {
const response = await fetch('/api/vendors');
const data = await response.json();
const select = document.getElementById('filter-vendor');
select.innerHTML = '<option value="">All Vendors</option>';
if (data.vendors) {
data.vendors.forEach(vendor => {
const option = document.createElement('option');
option.value = vendor.name;
option.textContent = vendor.name;
select.appendChild(option);
});
}
} catch (error) {
console.error('Error loading vendors dropdown:', error);
}
}
function filterByVendor(vendor) {
// Switch to posts tab
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-btn')[2].classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById('posts-tab').classList.add('active');
// Set filter and load
document.getElementById('filter-vendor').value = vendor;
postsOffset = 0;
hasMorePosts = true;
loadPosts();
}
// New Post Tab
function resetNewPostForm() {
document.getElementById('new-post-header').textContent = 'Create New Post';
document.getElementById('edit-post-id').value = '';
document.getElementById('post-title').value = '';
document.getElementById('post-vendor').value = '';
document.getElementById('post-sku').value = '';
document.getElementById('post-handle').value = '';
document.getElementById('post-image').value = '';
document.getElementById('post-tags').value = '';
document.getElementById('post-schedule').value = '';
document.getElementById('post-status').value = 'draft';
document.getElementById('seo-title').value = '';
document.getElementById('seo-description').value = '';
document.getElementById('post-body').value = '';
document.getElementById('image-preview').style.display = 'none';
document.getElementById('preview-container').style.display = 'none';
updateCharCounter('seo-title', 70);
updateCharCounter('seo-description', 160);
}
function previewImage() {
const url = document.getElementById('post-image').value;
const preview = document.getElementById('image-preview');
if (url) {
preview.src = url;
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
}
function updateCharCounter(fieldId, maxLength) {
const field = document.getElementById(fieldId);
const counter = document.getElementById(fieldId + '-counter');
const length = field.value.length;
counter.textContent = `${length} / ${maxLength} characters`;
if (length > maxLength) {
counter.classList.add('over');
} else {
counter.classList.remove('over');
}
}
function togglePreview() {
const container = document.getElementById('preview-container');
const body = document.getElementById('post-body').value;
if (container.style.display === 'none') {
container.style.display = 'block';
container.innerHTML = body || '<em>No content to preview</em>';
} else {
container.style.display = 'none';
}
}
async function generateContent() {
const title = document.getElementById('post-title').value;
const vendor = document.getElementById('post-vendor').value;
const sku = document.getElementById('post-sku').value;
const imageUrl = document.getElementById('post-image').value;
if (!title) {
showToast('Please enter a product title first', 'error');
return;
}
showToast('Generating content...', 'info');
try {
const response = await fetch('/api/generate-content', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, vendor, sku, image_url: imageUrl })
});
const data = await response.json();
if (response.ok) {
document.getElementById('seo-title').value = data.seo_title || '';
document.getElementById('seo-description').value = data.seo_description || '';
document.getElementById('post-body').value = data.body_html || '';
updateCharCounter('seo-title', 70);
updateCharCounter('seo-description', 160);
showToast('Content generated successfully', 'success');
} else {
showToast(data.error || 'Failed to generate content', 'error');
}
} catch (error) {
console.error('Error generating content:', error);
showToast('Error generating content', 'error');
}
}
async function savePost() {
const postId = document.getElementById('edit-post-id').value;
const title = document.getElementById('post-title').value;
if (!title) {
showToast('Title is required', 'error');
return;
}
const postData = {
title,
vendor: document.getElementById('post-vendor').value,
dw_sku: document.getElementById('post-sku').value,
product_handle: document.getElementById('post-handle').value,
image_url: document.getElementById('post-image').value,
tags: document.getElementById('post-tags').value,
scheduled_at: document.getElementById('post-schedule').value || null,
status: document.getElementById('post-status').value,
seo_title: document.getElementById('seo-title').value,
seo_description: document.getElementById('seo-description').value,
body_html: document.getElementById('post-body').value
};
try {
const url = postId ? `/api/posts/${postId}` : '/api/posts';
const method = postId ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData)
});
if (response.ok) {
showToast(`Post ${postId ? 'updated' : 'created'} successfully`, 'success');
resetNewPostForm();
loadStats();
// Switch to posts tab
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-btn')[2].classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById('posts-tab').classList.add('active');
postsOffset = 0;
hasMorePosts = true;
loadPosts();
} else {
const data = await response.json();
showToast(data.error || 'Failed to save post', 'error');
}
} catch (error) {
console.error('Error saving post:', error);
showToast('Error saving post', 'error');
}
}
async function openInstagramImport() {
try {
const response = await fetch('/api/instagram/recent');
const data = await response.json();
if (!data.posts || data.posts.length === 0) {
showToast('No Instagram posts found', 'info');
return;
}
const content = `
<div class="instagram-list">
${data.posts.map(post => `
<div class="instagram-item" onclick="importInstagram('${post.id}')">
<img src="${escapeHtml(post.image_url || '')}" class="instagram-thumb" onerror="this.style.display='none'">
<div class="instagram-info">
<div class="instagram-title">${escapeHtml(post.product_title || 'Untitled')}</div>
<div class="instagram-vendor">${escapeHtml(post.vendor || 'No vendor')}</div>
<div class="instagram-date">${post.posted_at ? formatDate(post.posted_at) : ''}</div>
</div>
</div>
`).join('')}
</div>
`;
showModal('Import from Instagram', content);
} catch (error) {
console.error('Error loading Instagram posts:', error);
showToast('Error loading Instagram posts', 'error');
}
}
async function importInstagram(id) {
closeModal();
showToast('Importing from Instagram...', 'info');
try {
const response = await fetch(`/api/instagram/import/${id}`, {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
showToast('Imported successfully', 'success');
// Populate form
document.getElementById('post-title').value = data.post.title || '';
document.getElementById('post-vendor').value = data.post.vendor || '';
document.getElementById('post-sku').value = data.post.dw_sku || '';
document.getElementById('post-image').value = data.post.image_url || '';
document.getElementById('post-body').value = data.post.body_html || '';
previewImage();
} else {
showToast(data.error || 'Failed to import', 'error');
}
} catch (error) {
console.error('Error importing Instagram post:', error);
showToast('Error importing post', 'error');
}
}
// Modal
function showModal(title, content) {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').innerHTML = content;
document.getElementById('modal').classList.add('active');
}
function closeModal() {
document.getElementById('modal').classList.remove('active');
}
// Toast
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: '✅',
error: '❌',
info: 'ℹ️'
};
toast.innerHTML = `
<div class="toast-icon">${icons[type] || 'ℹ️'}</div>
<div class="toast-message">${escapeHtml(message)}</div>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Utility functions
function formatDate(dateStr) {
const date = new Date(dateStr);
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return date.toLocaleDateString('en-US', options);
}
function formatDateForAPI(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>