Merge branch 'dev'
This commit is contained in:
commit
ea5336021d
|
|
@ -242,8 +242,74 @@ function renderCampaignPage(space: string): string {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="campaign-page__actions">
|
||||
<a href="/${space}/rsocials/thread" class="campaign-action-btn campaign-action-btn--outline">Open Thread Builder</a>
|
||||
<button class="campaign-action-btn campaign-action-btn--primary" id="import-md-btn">Import from Markdown</button>
|
||||
</div>
|
||||
${phaseHTML}
|
||||
</div>`;
|
||||
<div id="imported-posts"></div>
|
||||
</div>
|
||||
<div class="campaign-modal-overlay" id="import-modal" hidden>
|
||||
<div class="campaign-modal">
|
||||
<div class="campaign-modal__header">
|
||||
<h3>Import from Markdown</h3>
|
||||
<button class="campaign-modal__close" id="import-modal-close">×</button>
|
||||
</div>
|
||||
<textarea class="campaign-modal__textarea" id="import-md-textarea" placeholder="Paste tweets separated by ---\n\nFirst tweet\n---\nSecond tweet\n---\nThird tweet"></textarea>
|
||||
<div class="campaign-modal__row">
|
||||
<select class="campaign-modal__select" id="import-platform">
|
||||
<option value="twitter">Twitter / X</option>
|
||||
<option value="bluesky">Bluesky</option>
|
||||
<option value="mastodon">Mastodon</option>
|
||||
<option value="linkedin">LinkedIn</option>
|
||||
</select>
|
||||
<button class="campaign-action-btn campaign-action-btn--primary" id="import-parse-btn">Parse & Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
const modal = document.getElementById('import-modal');
|
||||
const openBtn = document.getElementById('import-md-btn');
|
||||
const closeBtn = document.getElementById('import-modal-close');
|
||||
const parseBtn = document.getElementById('import-parse-btn');
|
||||
const mdInput = document.getElementById('import-md-textarea');
|
||||
const platformSel = document.getElementById('import-platform');
|
||||
const importedEl = document.getElementById('imported-posts');
|
||||
|
||||
openBtn.addEventListener('click', () => { modal.hidden = false; });
|
||||
closeBtn.addEventListener('click', () => { modal.hidden = true; });
|
||||
modal.addEventListener('click', (e) => { if (e.target === modal) modal.hidden = true; });
|
||||
|
||||
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
parseBtn.addEventListener('click', () => {
|
||||
const raw = mdInput.value;
|
||||
const tweets = raw.split(/\\n---\\n/).map(t => t.trim()).filter(Boolean);
|
||||
if (!tweets.length) return;
|
||||
const platform = platformSel.value;
|
||||
const total = tweets.length;
|
||||
|
||||
let html = '<div class="campaign-phase"><h3 class="campaign-phase__title">📥 Imported Posts (' + total + ')</h3>';
|
||||
html += '<div class="campaign-phase__posts">';
|
||||
tweets.forEach((text, i) => {
|
||||
const preview = text.length > 180 ? esc(text.substring(0, 180)) + '...' : esc(text);
|
||||
html += '<div class="campaign-post">' +
|
||||
'<div class="campaign-post__header">' +
|
||||
'<span class="campaign-post__platform" style="background:#6366f1">' + esc(platform.charAt(0).toUpperCase()) + '</span>' +
|
||||
'<div class="campaign-post__meta"><strong>' + esc(platform) + '</strong></div>' +
|
||||
'<span class="campaign-status campaign-status--draft">imported</span>' +
|
||||
'</div>' +
|
||||
'<div class="campaign-post__step">Tweet ' + (i + 1) + '/' + total + '</div>' +
|
||||
'<p class="campaign-post__content">' + preview.replace(/\\n/g, '<br>') + '</p>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
importedEl.innerHTML = html;
|
||||
modal.hidden = true;
|
||||
});
|
||||
})();
|
||||
</script>`;
|
||||
}
|
||||
|
||||
const CAMPAIGN_CSS = `
|
||||
|
|
@ -277,6 +343,44 @@ const CAMPAIGN_CSS = `
|
|||
.campaign-post__content { font-size: 0.8rem; color: #94a3b8; line-height: 1.5; margin: 0 0 0.5rem; }
|
||||
.campaign-post__tags { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
.campaign-tag { font-size: 0.65rem; color: #7dd3fc; }
|
||||
.campaign-page__actions { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.campaign-action-btn {
|
||||
padding: 0.5rem 1rem; border-radius: 8px; font-size: 0.85rem; font-weight: 600;
|
||||
cursor: pointer; transition: all 0.15s; text-decoration: none; display: inline-flex; align-items: center;
|
||||
}
|
||||
.campaign-action-btn--primary { background: #6366f1; color: white; border: none; }
|
||||
.campaign-action-btn--primary:hover { background: #818cf8; }
|
||||
.campaign-action-btn--outline { background: transparent; color: #94a3b8; border: 1px solid #334155; }
|
||||
.campaign-action-btn--outline:hover { border-color: #6366f1; color: #c4b5fd; }
|
||||
.campaign-modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex;
|
||||
align-items: center; justify-content: center; z-index: 1000;
|
||||
}
|
||||
.campaign-modal-overlay[hidden] { display: none; }
|
||||
.campaign-modal {
|
||||
background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem;
|
||||
padding: 1.5rem; width: 90%; max-width: 540px; display: flex; flex-direction: column; gap: 1rem;
|
||||
}
|
||||
.campaign-modal__header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.campaign-modal__header h3 { margin: 0; font-size: 1.1rem; color: #f1f5f9; }
|
||||
.campaign-modal__close {
|
||||
background: none; border: none; color: #64748b; font-size: 1.5rem; cursor: pointer;
|
||||
line-height: 1; padding: 0;
|
||||
}
|
||||
.campaign-modal__close:hover { color: #e2e8f0; }
|
||||
.campaign-modal__textarea {
|
||||
width: 100%; min-height: 200px; background: #0f172a; color: #e2e8f0; border: 1px solid #334155;
|
||||
border-radius: 8px; padding: 0.75rem; font-family: inherit; font-size: 0.85rem; resize: vertical;
|
||||
line-height: 1.5; box-sizing: border-box;
|
||||
}
|
||||
.campaign-modal__textarea:focus { outline: none; border-color: #6366f1; }
|
||||
.campaign-modal__textarea::placeholder { color: #475569; }
|
||||
.campaign-modal__row { display: flex; gap: 0.75rem; align-items: center; }
|
||||
.campaign-modal__select {
|
||||
flex: 1; background: #0f172a; color: #e2e8f0; border: 1px solid #334155;
|
||||
border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.85rem;
|
||||
}
|
||||
.campaign-modal__select:focus { outline: none; border-color: #6366f1; }
|
||||
`;
|
||||
|
||||
routes.get("/campaign", (c) => {
|
||||
|
|
@ -292,6 +396,183 @@ routes.get("/campaign", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Thread Builder ──
|
||||
const THREAD_CSS = `
|
||||
.thread-page { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; max-width: 1100px; margin: 0 auto; padding: 2rem 1rem; min-height: 80vh; }
|
||||
.thread-page__header { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; }
|
||||
.thread-page__header h1 { margin: 0; font-size: 1.5rem; color: #f1f5f9; background: linear-gradient(135deg, #7dd3fc, #c4b5fd); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.thread-btn { padding: 0.5rem 1rem; border-radius: 8px; border: none; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.15s; }
|
||||
.thread-btn--primary { background: #6366f1; color: white; }
|
||||
.thread-btn--primary:hover { background: #818cf8; }
|
||||
.thread-btn--outline { background: transparent; color: #94a3b8; border: 1px solid #334155; }
|
||||
.thread-btn--outline:hover { border-color: #6366f1; color: #c4b5fd; }
|
||||
.thread-compose { position: sticky; top: 1rem; align-self: start; display: flex; flex-direction: column; gap: 1rem; }
|
||||
.thread-compose__textarea {
|
||||
width: 100%; min-height: 320px; background: #1e293b; color: #e2e8f0; border: 1px solid #334155;
|
||||
border-radius: 0.75rem; padding: 1rem; font-family: inherit; font-size: 0.9rem; resize: vertical;
|
||||
line-height: 1.6; box-sizing: border-box;
|
||||
}
|
||||
.thread-compose__textarea:focus { outline: none; border-color: #6366f1; }
|
||||
.thread-compose__textarea::placeholder { color: #475569; }
|
||||
.thread-compose__fields { display: flex; gap: 0.75rem; }
|
||||
.thread-compose__input {
|
||||
flex: 1; background: #1e293b; color: #e2e8f0; border: 1px solid #334155;
|
||||
border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.85rem; box-sizing: border-box;
|
||||
}
|
||||
.thread-compose__input:focus { outline: none; border-color: #6366f1; }
|
||||
.thread-compose__input::placeholder { color: #475569; }
|
||||
.thread-preview { display: flex; flex-direction: column; gap: 0; }
|
||||
.thread-preview__empty { color: #475569; text-align: center; padding: 3rem 1rem; font-size: 0.9rem; }
|
||||
.tweet-card {
|
||||
position: relative; background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem;
|
||||
padding: 1rem; margin-bottom: 0;
|
||||
}
|
||||
.tweet-card + .tweet-card { border-top-left-radius: 0; border-top-right-radius: 0; margin-top: -1px; }
|
||||
.tweet-card:has(+ .tweet-card) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
||||
.tweet-card__connector {
|
||||
position: absolute; left: 29px; top: -1px; width: 2px; height: 1rem;
|
||||
background: #334155; z-index: 1;
|
||||
}
|
||||
.tweet-card__header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.tweet-card__avatar {
|
||||
width: 40px; height: 40px; border-radius: 50%; background: #6366f1;
|
||||
display: flex; align-items: center; justify-content: center; color: white;
|
||||
font-weight: 700; font-size: 1rem; flex-shrink: 0;
|
||||
}
|
||||
.tweet-card__name { font-weight: 700; color: #f1f5f9; font-size: 0.9rem; }
|
||||
.tweet-card__handle { color: #64748b; font-size: 0.85rem; }
|
||||
.tweet-card__dot { color: #64748b; font-size: 0.85rem; }
|
||||
.tweet-card__time { color: #64748b; font-size: 0.85rem; }
|
||||
.tweet-card__content { color: #e2e8f0; font-size: 0.95rem; line-height: 1.6; margin: 0 0 0.75rem; white-space: pre-wrap; word-break: break-word; }
|
||||
.tweet-card__footer { display: flex; align-items: center; justify-content: space-between; }
|
||||
.tweet-card__actions { display: flex; gap: 1.25rem; }
|
||||
.tweet-card__action { display: flex; align-items: center; gap: 0.3rem; color: #64748b; font-size: 0.8rem; cursor: default; }
|
||||
.tweet-card__action svg { width: 16px; height: 16px; }
|
||||
.tweet-card__meta { display: flex; align-items: center; gap: 0.75rem; font-size: 0.75rem; color: #64748b; }
|
||||
.tweet-card__chars { font-variant-numeric: tabular-nums; }
|
||||
.tweet-card__chars--over { color: #ef4444; font-weight: 600; }
|
||||
.tweet-card__thread-num { color: #6366f1; font-weight: 600; }
|
||||
@media (max-width: 700px) {
|
||||
.thread-page { grid-template-columns: 1fr; }
|
||||
.thread-compose { position: static; }
|
||||
}
|
||||
`;
|
||||
|
||||
function renderThreadBuilderPage(space: string): string {
|
||||
return `
|
||||
<div class="thread-page">
|
||||
<div class="thread-page__header">
|
||||
<h1>Thread Builder</h1>
|
||||
<button class="thread-btn thread-btn--primary" id="thread-copy">Copy Thread</button>
|
||||
</div>
|
||||
<div class="thread-compose">
|
||||
<textarea class="thread-compose__textarea" id="thread-input" placeholder="Write your tweets here, separated by ---\n\nExample:\nFirst tweet goes here\n---\nSecond tweet\n---\nThird tweet"></textarea>
|
||||
<div class="thread-compose__fields">
|
||||
<input class="thread-compose__input" id="thread-name" placeholder="Display name" value="Your Name">
|
||||
<input class="thread-compose__input" id="thread-handle" placeholder="@handle" value="@yourhandle">
|
||||
</div>
|
||||
</div>
|
||||
<div class="thread-preview" id="thread-preview">
|
||||
<div class="thread-preview__empty">Your tweet thread preview will appear here</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module">
|
||||
const textarea = document.getElementById('thread-input');
|
||||
const preview = document.getElementById('thread-preview');
|
||||
const nameInput = document.getElementById('thread-name');
|
||||
const handleInput = document.getElementById('thread-handle');
|
||||
const copyBtn = document.getElementById('thread-copy');
|
||||
|
||||
const svgReply = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>';
|
||||
const svgRetweet = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><path d="M7 23l-4-4 4-4"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>';
|
||||
const svgHeart = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>';
|
||||
const svgShare = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>';
|
||||
|
||||
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
function renderPreview() {
|
||||
const raw = textarea.value;
|
||||
const tweets = raw.split(/\\n---\\n/).map(t => t.trim()).filter(Boolean);
|
||||
const name = nameInput.value || 'Your Name';
|
||||
const handle = handleInput.value || '@yourhandle';
|
||||
const initial = name.charAt(0).toUpperCase();
|
||||
const total = tweets.length;
|
||||
|
||||
if (!total) {
|
||||
preview.innerHTML = '<div class="thread-preview__empty">Your tweet thread preview will appear here</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
preview.innerHTML = tweets.map((text, i) => {
|
||||
const len = text.length;
|
||||
const overClass = len > 280 ? ' tweet-card__chars--over' : '';
|
||||
const connector = i > 0 ? '<div class="tweet-card__connector"></div>' : '';
|
||||
return '<div class="tweet-card">' +
|
||||
connector +
|
||||
'<div class="tweet-card__header">' +
|
||||
'<div class="tweet-card__avatar">' + esc(initial) + '</div>' +
|
||||
'<span class="tweet-card__name">' + esc(name) + '</span>' +
|
||||
'<span class="tweet-card__handle">' + esc(handle) + '</span>' +
|
||||
'<span class="tweet-card__dot">·</span>' +
|
||||
'<span class="tweet-card__time">now</span>' +
|
||||
'</div>' +
|
||||
'<p class="tweet-card__content">' + esc(text) + '</p>' +
|
||||
'<div class="tweet-card__footer">' +
|
||||
'<div class="tweet-card__actions">' +
|
||||
'<span class="tweet-card__action">' + svgReply + '</span>' +
|
||||
'<span class="tweet-card__action">' + svgRetweet + '</span>' +
|
||||
'<span class="tweet-card__action">' + svgHeart + '</span>' +
|
||||
'<span class="tweet-card__action">' + svgShare + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="tweet-card__meta">' +
|
||||
'<span class="tweet-card__chars' + overClass + '">' + len + '/280</span>' +
|
||||
'<span class="tweet-card__thread-num">' + (i + 1) + '/' + total + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
textarea.addEventListener('input', renderPreview);
|
||||
nameInput.addEventListener('input', renderPreview);
|
||||
handleInput.addEventListener('input', renderPreview);
|
||||
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
const tweets = textarea.value.split(/\\n---\\n/).map(t => t.trim()).filter(Boolean);
|
||||
if (!tweets.length) return;
|
||||
const total = tweets.length;
|
||||
const text = tweets.map((t, i) => (i + 1) + '/' + total + '\\n' + t).join('\\n\\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(() => { copyBtn.textContent = 'Copy Thread'; }, 2000);
|
||||
} catch(e) {
|
||||
copyBtn.textContent = 'Failed';
|
||||
setTimeout(() => { copyBtn.textContent = 'Copy Thread'; }, 2000);
|
||||
}
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
routes.get("/thread", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderShell({
|
||||
title: `Thread Builder — rSocials | rSpace`,
|
||||
moduleId: "rsocials",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: renderThreadBuilderPage(space),
|
||||
styles: `<style>${THREAD_CSS}</style>`,
|
||||
}));
|
||||
});
|
||||
|
||||
// ── Campaigns redirect (plural → singular) ──
|
||||
routes.get("/campaigns", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.redirect(`/${space}/rsocials/campaign`);
|
||||
});
|
||||
|
||||
// ── Page route ──
|
||||
routes.get("/", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
|
|
|
|||
Loading…
Reference in New Issue