mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(H1): per-page paywall toggle (gating intent)
Per-page editor gains a ⚡ Free / ⚡ N sats button that records the user's paywall intent on the manifest entry. Pages overview tiles surface a matching amber chip at the bottom-left so authors can scan paywall coverage at a glance. - openBookPageGatingDialog(entryId) prompts for a price in sats (empty / "0" clears the gating). Records as entry.gating = { mode: 'premium', priceSats, previewWatermark }. - syncBookPageEditBar swaps the button's label, colour, and title based on entry.gating; visible only when editing a page. - renderBookPagesGrid paints a .bookPageTileGateChip on tiles with premium gating. Limitation (deferred): publishBookPageToNostr does not yet route premium pages through _publishPremiumDesign — for v1 we persist the intent on the manifest, and the page event still publishes via the free path. Wiring the encrypted-publish + Reader-Mode unlock CTA lands in a follow-up (H1.2 / H3). Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
2f15bdca17
commit
a0e84d50bc
1 file changed
+76
@@ -1770,6 +1770,13 @@
|
||||
.bookPageTile.reflowSegment {
|
||||
background: linear-gradient(135deg, #f8fafc 25%, transparent 25%, transparent 50%, #f8fafc 50%, #f8fafc 75%, transparent 75%) 0 0/8px 8px;
|
||||
}
|
||||
.bookPageTileGateChip {
|
||||
position: absolute; bottom: 4px; left: 4px;
|
||||
padding: 2px 8px; font-size: .7rem;
|
||||
background: #fef3c7; color: #92400e;
|
||||
border: 1px solid #fbbf24; border-radius: 999px;
|
||||
z-index: 5; pointer-events: none;
|
||||
}
|
||||
.bookPageTileUpdateChip {
|
||||
position: absolute; top: 4px; left: 4px;
|
||||
padding: 2px 8px; font-size: .72rem;
|
||||
@@ -4667,6 +4674,7 @@
|
||||
<button type="button" id="bookMasterAddTextFrame" title="Add a reflow text-frame to this master" style="display:none">+ Text frame</button>
|
||||
<button type="button" id="bookPageEditPublish" title="Publish this page as a casewrap-page event">Publish</button>
|
||||
<button type="button" id="bookPageEditProofPdf" title="Export just this page as a PDF (single-page proof for the printer)">📄 Proof PDF</button>
|
||||
<button type="button" id="bookPageEditGatingBtn" title="Set premium / paywall on this page" style="display:none">⚡ Free</button>
|
||||
<span class="hint" id="bookPageEditPublishStatus" style="margin-left:6px;font-size:.74rem"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8863,6 +8871,44 @@
|
||||
// label. Layers for the master live in state.layers under
|
||||
// `book-master:<id>`. The first master created becomes the
|
||||
// default automatically.
|
||||
// Per-page paywall / premium gating (Phase H1). Records the
|
||||
// user's intent on the manifest entry — the actual encrypted-
|
||||
// publish wiring (route through _publishPremiumDesign for
|
||||
// premium pages) is a follow-up slice. For v1 we surface the
|
||||
// chip on tiles + the publish-modal note so authors can prep
|
||||
// their pricing.
|
||||
function openBookPageGatingDialog(entryId) {
|
||||
const book = state.book;
|
||||
const entry = book?.pages?.find(p => p.id === entryId);
|
||||
if (!entry) return;
|
||||
const isPremium = entry.gating?.mode === 'premium';
|
||||
const currentPrice = entry.gating?.priceSats || 1000;
|
||||
const choice = prompt(
|
||||
`Gating for "${entry.label || 'this page'}"\n` +
|
||||
`\n` +
|
||||
`Type a price in sats to mark this page premium (readers will see a watermarked preview + an "Unlock with zap" CTA in Reader Mode).\n` +
|
||||
`Type "0" or leave empty to keep it free.\n` +
|
||||
`\n` +
|
||||
`Note: v1 persists the intent on the manifest; full encrypted-publish wiring lands in H1.2.`,
|
||||
isPremium ? String(currentPrice) : ''
|
||||
);
|
||||
if (choice === null) return; // cancelled
|
||||
const trimmed = String(choice).trim();
|
||||
pushHistory('book: page gating');
|
||||
if (!trimmed || trimmed === '0') {
|
||||
entry.gating = null;
|
||||
} else {
|
||||
const n = Math.max(1, Math.floor(Number(trimmed) || 0));
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
toastError && toastError('Invalid price — enter a positive number of sats or 0 for free.');
|
||||
return;
|
||||
}
|
||||
entry.gating = { mode: 'premium', priceSats: n, previewWatermark: true };
|
||||
}
|
||||
render();
|
||||
syncBookPageEditBar();
|
||||
}
|
||||
|
||||
// Convert the current deck into a book (Phase F2). Each slide
|
||||
// becomes a designed page; layers letterbox-fit into the trim
|
||||
// (contain-scaled + centered). Switches templateMode to 'book'.
|
||||
@@ -9624,6 +9670,15 @@
|
||||
});
|
||||
actions.appendChild(del);
|
||||
tile.appendChild(actions);
|
||||
// Premium chip (Phase H1). Surfaces the gating intent on the
|
||||
// tile so authors can scan paywall coverage at a glance.
|
||||
if (p.gating?.mode === 'premium') {
|
||||
const gateChip = document.createElement('div');
|
||||
gateChip.className = 'bookPageTileGateChip';
|
||||
gateChip.textContent = `⚡ ${p.gating.priceSats || 0}`;
|
||||
gateChip.title = `Premium — ${p.gating.priceSats || 0} sats`;
|
||||
tile.appendChild(gateChip);
|
||||
}
|
||||
// Upstream-update chip (D1.7). Shown when the resolver's
|
||||
// background probe found a newer version of this entry's
|
||||
// ref upstream. Click applies it.
|
||||
@@ -11503,6 +11558,7 @@
|
||||
if (next) next.style.display = '';
|
||||
if (tfBtn) tfBtn.style.display = 'none';
|
||||
if (back) back.textContent = '← Back to Pages';
|
||||
const gateBtn = document.getElementById('bookPageEditGatingBtn');
|
||||
if (pubBtn) {
|
||||
const entry = book.pages.find(p => p.id === book.editingEntryId);
|
||||
const visTag = state.visibility === 'private' ? ' 🔒' : '';
|
||||
@@ -11510,6 +11566,22 @@
|
||||
pubBtn.title = entry?.ref
|
||||
? `Replace ${entry.ref.dTag} on relays${visTag ? ' (private)' : ''}`
|
||||
: `Publish this page as a casewrap-page event${visTag ? ' (private, end-to-end encrypted)' : ''}`;
|
||||
if (gateBtn) {
|
||||
gateBtn.style.display = '';
|
||||
if (entry?.gating?.mode === 'premium') {
|
||||
gateBtn.textContent = `⚡ ${entry.gating.priceSats || 0} sats`;
|
||||
gateBtn.title = `Premium page — readers pay ${entry.gating.priceSats || 0} sats to unlock. Click to change.`;
|
||||
gateBtn.style.background = '#fef3c7';
|
||||
gateBtn.style.color = '#92400e';
|
||||
gateBtn.style.borderColor = '#fbbf24';
|
||||
} else {
|
||||
gateBtn.textContent = '⚡ Free';
|
||||
gateBtn.title = 'Click to add a paywall for this page (premium / zap to unlock)';
|
||||
gateBtn.style.background = '';
|
||||
gateBtn.style.color = '';
|
||||
gateBtn.style.borderColor = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
const designed = book.pages.filter(p => p.type === 'design');
|
||||
const idx = designed.findIndex(p => p.id === book.editingEntryId);
|
||||
@@ -35921,6 +35993,10 @@
|
||||
on('bookPageEditNext', 'click', () => gotoBookPageRelative(1));
|
||||
on('bookMasterAddTextFrame', 'click', () => addBookTextFrameToActiveMaster());
|
||||
on('bookPageEditPublish', 'click', publishBookSurface);
|
||||
on('bookPageEditGatingBtn', 'click', () => {
|
||||
const id = state.book?.editingEntryId;
|
||||
if (id) openBookPageGatingDialog(id);
|
||||
});
|
||||
on('bookPageEditProofPdf', 'click', async () => {
|
||||
const book = state.book;
|
||||
const entryId = book?.editingEntryId;
|
||||
|
||||
Reference in new issue
Block a user