@mevdragon/vidfarm-devcli 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/vidfarm-media/SKILL.md +41 -4
- package/SKILL.director.md +17 -3
- package/SKILL.platform.md +3 -3
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1550 -187
- package/dist/src/cli.js +227 -7
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +52 -9
- package/dist/src/devcli/composition-edit.js +262 -0
- package/dist/src/devcli/timeline-edit.js +283 -0
- package/dist/src/editor-chat.js +29 -6
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +744 -0
- package/dist/src/frontend/template-editor-chat.js +22 -19
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +25 -1
- package/dist/src/reskin/agency-page.js +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +319 -72
- package/dist/src/reskin/discover-page.js +486 -31
- package/dist/src/reskin/document.js +1171 -384
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-page.js +10 -4
- package/dist/src/reskin/library-page.js +918 -220
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +337 -17
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +79 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +14 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +114 -0
- package/dist/src/services/storage.js +24 -1
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
// live editorHref) and override any endpoint.
|
|
42
42
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
43
43
|
import { POPULAR_INSPIRATION_GROUPS } from "../frontend/homepage-shared.js";
|
|
44
|
+
/** Config-only defaults (the data fields feedInitial/feedFetchedAt are seeded
|
|
45
|
+
* per-request, never defaulted). */
|
|
44
46
|
const DEFAULT_INPUT = {
|
|
45
47
|
accountId: "",
|
|
46
48
|
isLoggedIn: false,
|
|
@@ -69,6 +71,8 @@ const MODE_COPY = {
|
|
|
69
71
|
};
|
|
70
72
|
// Functional (non-decorative) SVG glyphs.
|
|
71
73
|
const EDIT_ICON = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>`;
|
|
74
|
+
const COPY_ICON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/></svg>`;
|
|
75
|
+
const IDCHECK_ICON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12.5l5 5 10-11"/></svg>`;
|
|
72
76
|
const BOOKMARK_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21l-5-3.4L7 21V5.2A2.2 2.2 0 0 1 9.2 3h5.6A2.2 2.2 0 0 1 17 5.2z"/></svg>`;
|
|
73
77
|
const CHECK_ICON = `<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12.5l5 5 10-11"/></svg>`;
|
|
74
78
|
const CROSS_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6L6 18"/></svg>`;
|
|
@@ -77,6 +81,8 @@ const SOUND_OFF_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="no
|
|
|
77
81
|
const CHEV_UP_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 14.5 7-7 7 7"/></svg>`;
|
|
78
82
|
const CHEV_DOWN_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 9.5 7 7 7-7"/></svg>`;
|
|
79
83
|
const PLAY_ICON = `<svg width="56" height="56" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.4v13.2L19 12z"/></svg>`;
|
|
84
|
+
const PLUS_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
85
|
+
const UPLOAD_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/></svg>`;
|
|
80
86
|
/** Safe JSON embedding inside a <script> tag (mirrors page-shell.escapeJsonForHtml). */
|
|
81
87
|
function jsonForScript(value) {
|
|
82
88
|
return JSON.stringify(value ?? null)
|
|
@@ -90,7 +96,7 @@ function jsonForScript(value) {
|
|
|
90
96
|
function modeToggle(active) {
|
|
91
97
|
const links = MODES.map((m) => {
|
|
92
98
|
const on = m.mode === active ? " is-active" : "";
|
|
93
|
-
return `<a class="rk-discover-mode${on}" href="/discover
|
|
99
|
+
return `<a class="rk-discover-mode${on}" href="/discover/${m.mode}"${m.mode === active ? ' aria-current="page"' : ""}>${m.label}</a>`;
|
|
94
100
|
}).join("");
|
|
95
101
|
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
96
102
|
}
|
|
@@ -139,9 +145,59 @@ function viewerHtml() {
|
|
|
139
145
|
</div>
|
|
140
146
|
</div>`;
|
|
141
147
|
}
|
|
148
|
+
// The "Add New" modal (feed mode only) — paste a social video URL or upload a
|
|
149
|
+
// video, adding it to the caller's private Discover feed via the same real
|
|
150
|
+
// endpoints the old React SPA used (POST /discover/templates + the upload
|
|
151
|
+
// presign pair). Mirrors the old UI's Add Template modal, farmville-styled.
|
|
152
|
+
function addModalHtml() {
|
|
153
|
+
return `
|
|
154
|
+
<div class="rk-add-modal" data-rk-add-modal hidden role="dialog" aria-modal="true" aria-labelledby="rk-add-title">
|
|
155
|
+
<div class="rk-add-scrim" data-rk-add-close></div>
|
|
156
|
+
<div class="rk-add-card">
|
|
157
|
+
<div class="rk-add-head">
|
|
158
|
+
<h2 class="rk-add-title" id="rk-add-title">Add to your feed</h2>
|
|
159
|
+
<button type="button" class="rk-add-x" data-rk-add-close aria-label="Close">${CROSS_ICON}</button>
|
|
160
|
+
</div>
|
|
161
|
+
<p class="rk-add-sub">Paste a TikTok, YouTube, Twitter/X, or Instagram video link — or upload a video from your computer. It's added to your Discover feed, visible only to your account.</p>
|
|
162
|
+
<form class="rk-add-form" data-rk-add-form>
|
|
163
|
+
<div class="rk-add-field">
|
|
164
|
+
<label class="rk-add-label" for="rk-add-url">Video URL</label>
|
|
165
|
+
<input class="rk-add-input" id="rk-add-url" type="url" placeholder="https://www.tiktok.com/@…" autocomplete="off" data-rk-add-url />
|
|
166
|
+
</div>
|
|
167
|
+
<div class="rk-add-or"><span>or</span></div>
|
|
168
|
+
<label class="rk-add-file" data-rk-add-filelabel>
|
|
169
|
+
<input type="file" accept="video/*,.mp4,.mov,.webm,.m4v" data-rk-add-file />
|
|
170
|
+
<span class="rk-add-file-ic">${UPLOAD_ICON}</span>
|
|
171
|
+
<span class="rk-add-file-text" data-rk-add-filetext>Upload a video<span class="rk-add-optional">MP4 / MOV / WebM, up to 200 MB</span></span>
|
|
172
|
+
</label>
|
|
173
|
+
<div class="rk-add-field">
|
|
174
|
+
<label class="rk-add-label" for="rk-add-tagline">Tagline <span class="rk-add-optional">optional</span></label>
|
|
175
|
+
<input class="rk-add-input" id="rk-add-tagline" type="text" placeholder="What makes it work" autocomplete="off" data-rk-add-tagline />
|
|
176
|
+
</div>
|
|
177
|
+
<div class="rk-add-field">
|
|
178
|
+
<label class="rk-add-label" for="rk-add-notes">Notes <span class="rk-add-optional">optional</span></label>
|
|
179
|
+
<textarea class="rk-add-input rk-add-textarea" id="rk-add-notes" rows="2" placeholder="Anything to remember about this one" data-rk-add-notes></textarea>
|
|
180
|
+
</div>
|
|
181
|
+
<p class="rk-add-error" data-rk-add-error hidden></p>
|
|
182
|
+
<div class="rk-add-actions">
|
|
183
|
+
<button type="button" class="rk-btn rk-btn-ghost" data-rk-add-close>Cancel</button>
|
|
184
|
+
<button type="submit" class="rk-btn rk-btn-gold" data-rk-add-submit>Add to feed</button>
|
|
185
|
+
</div>
|
|
186
|
+
</form>
|
|
187
|
+
</div>
|
|
188
|
+
</div>`;
|
|
189
|
+
}
|
|
142
190
|
export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
143
191
|
const copy = MODE_COPY[mode];
|
|
144
|
-
|
|
192
|
+
// feedInitial/feedFetchedAt only make sense in feed mode; drop them elsewhere
|
|
193
|
+
// so swipe/categories boots stay lean.
|
|
194
|
+
const { feedInitial, feedFetchedAt, ...cfg } = input;
|
|
195
|
+
const boot = {
|
|
196
|
+
...DEFAULT_INPUT,
|
|
197
|
+
...cfg,
|
|
198
|
+
feedInitial: mode === "feed" ? feedInitial ?? null : null,
|
|
199
|
+
feedFetchedAt: mode === "feed" ? feedFetchedAt ?? null : null
|
|
200
|
+
};
|
|
145
201
|
let modeBody = "";
|
|
146
202
|
// ── FEED ────────────────────────────────────────────────────────────────
|
|
147
203
|
if (mode === "feed") {
|
|
@@ -158,8 +214,7 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
158
214
|
<span class="rk-discover-search-ic" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8.5" cy="8.5" r="5.8"/><path d="m13 13 4.2 4.2"/></svg></span>
|
|
159
215
|
<input type="search" data-rk-search placeholder="Search templates" autocomplete="off" aria-label="Search templates" />
|
|
160
216
|
</label>
|
|
161
|
-
<button type="button" class="rk-discover-bmfilter" data-rk-bmfilter aria-pressed="false" title="Show bookmarked templates only">${BOOKMARK_ICON}<span class="rk-discover-bmfilter-count" data-rk-bmcount></span></button
|
|
162
|
-
<span class="rk-hint rk-discover-count" data-rk-count></span>`
|
|
217
|
+
<button type="button" class="rk-discover-bmfilter" data-rk-bmfilter aria-pressed="false" title="Show bookmarked templates only">${BOOKMARK_ICON}<span class="rk-discover-bmfilter-count" data-rk-bmcount></span></button>`
|
|
163
218
|
: "";
|
|
164
219
|
// Categories' in-page group reveal header ("← All categories" + the group
|
|
165
220
|
// title) lives IN the toolbar row, next to the mode toggle (user feedback);
|
|
@@ -177,6 +232,7 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
177
232
|
<div class="rk-discover-stage">
|
|
178
233
|
<button type="button" class="rk-discover-act rk-discover-act-skip" data-swipe-skip data-swipe-act hidden aria-label="Skip">${CROSS_ICON}</button>
|
|
179
234
|
<div class="rk-discover-deck" data-deck>
|
|
235
|
+
<button type="button" class="rk-discover-sound-toggle" data-swipe-sound hidden aria-label="Unmute" aria-pressed="false">${SOUND_OFF_ICON}</button>
|
|
180
236
|
<div class="rk-discover-swipe-state" data-rk-swipe-state>Loading your deck…</div>
|
|
181
237
|
</div>
|
|
182
238
|
<button type="button" class="rk-discover-act rk-discover-act-fork" data-swipe-fork data-swipe-act hidden aria-label="Fork">${CHECK_ICON}</button>
|
|
@@ -195,11 +251,21 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
195
251
|
<div class="rk-discover-state rk-discover-cats-state" data-rk-cats-state hidden></div>
|
|
196
252
|
</div>${viewerHtml()}`;
|
|
197
253
|
}
|
|
254
|
+
// "Add New" lives top-right of the page head (feed mode only) — opens the
|
|
255
|
+
// Add modal when signed in, else routes to /login (mirrors the old UI).
|
|
256
|
+
const addNewBtn = mode === "feed"
|
|
257
|
+
? (boot.isLoggedIn
|
|
258
|
+
? `<button type="button" class="rk-btn rk-btn-gold rk-discover-add" data-rk-add>${PLUS_ICON}<span>Add New</span></button>`
|
|
259
|
+
: `<a class="rk-btn rk-btn-gold rk-discover-add" href="/login">${PLUS_ICON}<span>Add New</span></a>`)
|
|
260
|
+
: "";
|
|
198
261
|
const body = `
|
|
199
262
|
<main class="rk-container rk-page">
|
|
200
263
|
<div class="rk-page-head rk-discover-head">
|
|
201
|
-
<
|
|
202
|
-
|
|
264
|
+
<div class="rk-discover-headtext">
|
|
265
|
+
<h1 class="rk-h1">Discover</h1>
|
|
266
|
+
<p class="rk-sub">${copy.sub}</p>
|
|
267
|
+
</div>
|
|
268
|
+
${addNewBtn}
|
|
203
269
|
</div>
|
|
204
270
|
<div class="rk-discover-toolbar">
|
|
205
271
|
${modeToggle(mode)}
|
|
@@ -207,10 +273,58 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
207
273
|
${toolbarSearch}
|
|
208
274
|
</div>
|
|
209
275
|
${modeBody}
|
|
276
|
+
${mode === "feed" ? addModalHtml() : ""}
|
|
210
277
|
<script type="application/json" data-rk-discover-boot>${jsonForScript(boot)}</script>
|
|
211
278
|
</main>`;
|
|
212
279
|
const pageCss = `
|
|
213
|
-
|
|
280
|
+
/* head row: title block on the left, "Add New" CTA pinned top-right */
|
|
281
|
+
.rk-discover-head{margin-bottom:22px;display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
|
|
282
|
+
.rk-discover-headtext{display:grid;gap:4px;min-width:0}
|
|
283
|
+
.rk-discover-add{flex:none;align-self:flex-start}
|
|
284
|
+
.rk-discover-add svg{margin-top:-1px}
|
|
285
|
+
@media(max-width:520px){
|
|
286
|
+
.rk-discover-add span{display:none}
|
|
287
|
+
.rk-discover-add{padding:11px}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* ── "Add New" modal ── */
|
|
291
|
+
.rk-add-modal{position:fixed;inset:0;z-index:140;display:grid;place-items:center;padding:20px}
|
|
292
|
+
.rk-add-modal[hidden]{display:none}
|
|
293
|
+
.rk-add-scrim{position:absolute;inset:0;background:rgba(15,12,5,.5);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
|
|
294
|
+
.rk-add-card{position:relative;z-index:1;width:min(480px,100%);max-height:calc(100vh - 40px);overflow:auto;
|
|
295
|
+
background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-xl);padding:24px 24px 22px}
|
|
296
|
+
.rk-add-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px}
|
|
297
|
+
.rk-add-title{font-family:var(--rk-font-display);font-size:var(--rk-text-xl);font-weight:800;color:var(--rk-ink);letter-spacing:-.02em}
|
|
298
|
+
.rk-add-x{flex:none;width:34px;height:34px;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);
|
|
299
|
+
display:grid;place-items:center;cursor:pointer;transition:color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
300
|
+
.rk-add-x:hover{color:var(--rk-ink);border-color:var(--rk-border-strong)}
|
|
301
|
+
.rk-add-x svg{width:16px;height:16px}
|
|
302
|
+
.rk-add-sub{font-size:13px;color:var(--rk-text-muted);line-height:1.5;margin-bottom:18px}
|
|
303
|
+
.rk-add-form{display:grid;gap:14px}
|
|
304
|
+
.rk-add-field{display:grid;gap:6px}
|
|
305
|
+
.rk-add-label{font-size:12.5px;font-weight:700;color:var(--rk-n-700)}
|
|
306
|
+
.rk-add-optional{font-weight:500;color:var(--rk-text-faint)}
|
|
307
|
+
.rk-add-input{width:100%;box-sizing:border-box;border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);
|
|
308
|
+
background:#fff;padding:11px 14px;font-family:var(--rk-font-body);font-size:14px;color:var(--rk-ink);outline:none;
|
|
309
|
+
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
310
|
+
.rk-add-input::placeholder{color:var(--rk-n-400)}
|
|
311
|
+
.rk-add-input:focus{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
312
|
+
.rk-add-textarea{resize:vertical;min-height:52px;line-height:1.45}
|
|
313
|
+
.rk-add-or{display:flex;align-items:center;gap:12px;color:var(--rk-text-faint);font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
314
|
+
.rk-add-or::before,.rk-add-or::after{content:"";height:1px;flex:1;background:var(--rk-border)}
|
|
315
|
+
.rk-add-file{display:flex;align-items:center;gap:12px;border:1px dashed var(--rk-border-strong);border-radius:var(--rk-r-lg);
|
|
316
|
+
padding:13px 14px;cursor:pointer;color:var(--rk-n-700);
|
|
317
|
+
transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
318
|
+
.rk-add-file:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
319
|
+
.rk-add-file input[type=file]{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
|
|
320
|
+
.rk-add-file-ic{flex:none;color:var(--rk-gold-700);display:grid;place-items:center}
|
|
321
|
+
.rk-add-file-text{font-size:13.5px;font-weight:600;display:grid;gap:2px;min-width:0}
|
|
322
|
+
.rk-add-file-text .rk-add-optional{font-size:11.5px;font-weight:500}
|
|
323
|
+
.rk-add-file.is-picked{border-style:solid;border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
324
|
+
.rk-add-error{font-size:13px;color:#b91c1c;background:var(--rk-red-tint);border:1px solid #fecaca;border-radius:var(--rk-r-lg);padding:9px 12px;font-weight:500;line-height:1.4}
|
|
325
|
+
.rk-add-error[hidden]{display:none}
|
|
326
|
+
.rk-add-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
|
|
327
|
+
.rk-add-actions .rk-btn[disabled]{opacity:.6;cursor:default;transform:none}
|
|
214
328
|
|
|
215
329
|
/* toolbar row: mode toggle + (feed) quick search share one line, wrap on mobile */
|
|
216
330
|
.rk-discover-toolbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:26px}
|
|
@@ -233,11 +347,10 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
233
347
|
.rk-discover-search-ic{color:var(--rk-text-faint);display:grid;place-items:center;flex:none}
|
|
234
348
|
.rk-discover-search input{flex:1;min-width:0;border:0;outline:none;background:transparent;font-family:var(--rk-font-body);font-size:14px;color:var(--rk-ink)}
|
|
235
349
|
.rk-discover-search input::placeholder{color:var(--rk-n-400)}
|
|
236
|
-
.rk-discover-count{white-space:nowrap}
|
|
237
350
|
|
|
238
351
|
.rk-discover-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(212px,1fr));gap:22px}
|
|
239
352
|
@media(max-width:520px){.rk-discover-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}}
|
|
240
|
-
.rk-discover-card{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);overflow:hidden;box-shadow:var(--rk-shadow-card);display:flex;flex-direction:column}
|
|
353
|
+
.rk-discover-card{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);overflow:hidden;box-shadow:var(--rk-shadow-card);display:flex;flex-direction:column;min-width:0}
|
|
241
354
|
.rk-discover-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:var(--rk-n-100);color:inherit}
|
|
242
355
|
.rk-discover-poster img,.rk-discover-poster video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
243
356
|
.rk-discover-media-empty{position:absolute;inset:0;display:grid;place-items:center;font-size:12.5px;font-weight:600;color:var(--rk-text-faint);
|
|
@@ -252,15 +365,28 @@ export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
|
252
365
|
display:none;place-items:center;color:#fff;background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);cursor:pointer;
|
|
253
366
|
transition:transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
254
367
|
.rk-discover-posterwrap:hover .rk-discover-bm{display:grid}
|
|
368
|
+
/* touch has no hover, so always show the bookmark button there (else you can
|
|
369
|
+
never save a not-yet-bookmarked card from the grid) */
|
|
370
|
+
@media(hover:none){.rk-discover-bm{display:grid}}
|
|
255
371
|
.rk-discover-bm:hover{transform:scale(1.08);background:rgba(0,0,0,.6)}
|
|
256
372
|
.rk-discover-bm.is-on{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
257
373
|
.rk-discover-bm.is-on svg{fill:currentColor}
|
|
258
|
-
.rk-discover-body{padding:15px 16px 16px;display:grid;gap:13px}
|
|
259
|
-
.rk-discover-titlewrap{display:grid;gap:6px}
|
|
374
|
+
.rk-discover-body{padding:15px 16px 16px;display:grid;gap:13px;min-width:0}
|
|
375
|
+
.rk-discover-titlewrap{display:grid;gap:6px;min-width:0}
|
|
260
376
|
.rk-discover-title{font-family:var(--rk-font-display);font-size:1.02rem;font-weight:700;letter-spacing:-.01em;color:var(--rk-ink);line-height:1.2;
|
|
261
377
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
262
378
|
.rk-discover-desc{font-size:12.5px;font-weight:500;color:var(--rk-text-muted);line-height:1.4;
|
|
263
379
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
380
|
+
.rk-discover-idchip{justify-self:start;display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;
|
|
381
|
+
padding:3px 8px;border-radius:999px;background:var(--rk-surface-2,rgba(0,0,0,.05));
|
|
382
|
+
font-family:var(--rk-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;font-weight:600;
|
|
383
|
+
color:var(--rk-text-muted);cursor:pointer;border:0;line-height:1.4;transition:background .15s,color .15s;
|
|
384
|
+
-webkit-user-select:none;user-select:none;box-sizing:border-box}
|
|
385
|
+
.rk-discover-idchip:hover{background:var(--rk-surface-3,rgba(0,0,0,.09));color:var(--rk-ink)}
|
|
386
|
+
.rk-discover-idchip svg{flex:0 0 auto;opacity:.7}
|
|
387
|
+
.rk-discover-idchip .rk-idchip-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
388
|
+
.rk-discover-idchip.is-copied{background:rgba(52,168,110,.16);color:#1f7a4d}
|
|
389
|
+
.rk-discover-idchip.is-copied svg{opacity:1;color:#1f7a4d}
|
|
264
390
|
.rk-discover-fork{gap:7px}
|
|
265
391
|
.rk-discover-fork svg{margin-top:-1px}
|
|
266
392
|
.rk-discover-fork-disabled{opacity:.6;cursor:default;pointer-events:none}
|
|
@@ -358,12 +484,22 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
358
484
|
transition:transform .38s var(--rk-ease),opacity .38s var(--rk-ease)}
|
|
359
485
|
.rk-discover-swipe-frame{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;pointer-events:none;background:#000}
|
|
360
486
|
.rk-discover-swipe-fallback{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;
|
|
361
|
-
background:linear-gradient(155deg,#3a3a3a,#111);color:#fff;font-family:var(--rk-font-display);font-weight:700;font-size:1.1rem;line-height:1.15
|
|
487
|
+
background:linear-gradient(155deg,#3a3a3a,#111);color:#fff;font-family:var(--rk-font-display);font-weight:700;font-size:1.1rem;line-height:1.15;
|
|
488
|
+
transition:opacity .4s var(--rk-ease)}
|
|
362
489
|
.rk-discover-swipe-scrim{position:absolute;inset:0;z-index:1;pointer-events:none;background:linear-gradient(to top,rgba(0,0,0,.62) 0%,rgba(0,0,0,.16) 46%,transparent 74%)}
|
|
363
490
|
.rk-discover-swipe-info{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:20px;display:grid;gap:6px;pointer-events:none}
|
|
364
491
|
.rk-discover-swipe-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.05rem;color:#fff;letter-spacing:-.01em;line-height:1.18}
|
|
365
492
|
.rk-discover-swipe-caption{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.84);line-height:1.35;
|
|
366
493
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
494
|
+
/* audio toggle — overlays the top-of-deck card, top-right; global mute state */
|
|
495
|
+
.rk-discover-sound-toggle{position:absolute;top:12px;right:12px;z-index:40;width:38px;height:38px;border-radius:var(--rk-r-full);
|
|
496
|
+
border:0;display:grid;place-items:center;cursor:pointer;color:#fff;background:rgba(15,15,15,.5);backdrop-filter:blur(6px);
|
|
497
|
+
-webkit-backdrop-filter:blur(6px);box-shadow:0 2px 8px -2px rgba(0,0,0,.5);
|
|
498
|
+
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
499
|
+
.rk-discover-sound-toggle[hidden]{display:none}
|
|
500
|
+
.rk-discover-sound-toggle:hover{background:rgba(15,15,15,.72);transform:scale(1.06)}
|
|
501
|
+
.rk-discover-sound-toggle.is-on{background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
502
|
+
.rk-discover-sound-toggle.is-on:hover{background:var(--rk-gold-700)}
|
|
367
503
|
|
|
368
504
|
/* deck flanked by skip/fork — buttons ride the card's vertical center, 1fr gutters absorb spare width.
|
|
369
505
|
Explicit column placement keeps the deck centered when the buttons are display:none (empty/caught-up states). */
|
|
@@ -382,6 +518,17 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
382
518
|
.rk-discover-act-skip{grid-area:skip;justify-self:center}
|
|
383
519
|
.rk-discover-act-fork{grid-area:fork;justify-self:center}
|
|
384
520
|
}
|
|
521
|
+
/* short viewports (landscape phones, small laptops): the deck is sized by width
|
|
522
|
+
with a locked 9/16 ratio → ~570px tall, taller than the screen, so the card +
|
|
523
|
+
its flanking Skip/Fork controls fall off-screen. Cap it by HEIGHT instead and
|
|
524
|
+
let width follow the ratio (mirrors how the fullscreen viewer stage sizes). */
|
|
525
|
+
@media(max-height:720px){
|
|
526
|
+
.rk-discover-deck{height:min(74vh,572px);width:auto}
|
|
527
|
+
}
|
|
528
|
+
@media(max-height:520px){
|
|
529
|
+
.rk-discover-head{margin-bottom:10px}
|
|
530
|
+
.rk-discover-swipe-wrap{gap:10px;padding:0}
|
|
531
|
+
}
|
|
385
532
|
.rk-discover-act:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
|
|
386
533
|
.rk-discover-act:disabled{opacity:.5;cursor:default;transform:none}
|
|
387
534
|
.rk-discover-act-skip{color:var(--rk-n-500)}
|
|
@@ -447,6 +594,61 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
447
594
|
var u=editorBase+'/'+encodeURIComponent(String(id));
|
|
448
595
|
return accountId? u+'?account='+encodeURIComponent(accountId) : u;
|
|
449
596
|
}
|
|
597
|
+
// Feed cards browse INSPIRATIONS: an entry may have no template yet. The real
|
|
598
|
+
// template id (if decomposed) opens the editor directly; otherwise the card
|
|
599
|
+
// carries an inspirationId and its CTA decomposes on demand (preview → Remix).
|
|
600
|
+
function realTemplateId(t){ var id=t.templateId||t.template_id||''; return /^template_/.test(String(id))? String(id):''; }
|
|
601
|
+
function inspirationIdOf(t){ return String(t.inspirationId||t.inspiration_id||''); }
|
|
602
|
+
function domId(t){ return realTemplateId(t)||inspirationIdOf(t)||''; }
|
|
603
|
+
function needsDecompose(t){ return !realTemplateId(t) && Boolean(inspirationIdOf(t)); }
|
|
604
|
+
// POST the lazy decompose, then hand off to the editor. Shared by card +
|
|
605
|
+
// viewer Remix buttons via a delegated [data-remix] listener.
|
|
606
|
+
function remixInspiration(btn){
|
|
607
|
+
var inspId=btn.getAttribute('data-remix'); if(!inspId||btn.getAttribute('data-busy')) return;
|
|
608
|
+
btn.setAttribute('data-busy','1');
|
|
609
|
+
var label=btn.innerHTML; btn.innerHTML='Preparing…';
|
|
610
|
+
fetch(withAccount('/discover/inspirations/'+encodeURIComponent(inspId)+'/decompose'),{method:'POST',credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
611
|
+
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); })
|
|
612
|
+
.then(function(res){
|
|
613
|
+
if(res.ok&&res.data&&res.data.templateId){ window.location.href=editorUrl(res.data.templateId); return; }
|
|
614
|
+
btn.removeAttribute('data-busy'); btn.innerHTML=label;
|
|
615
|
+
window.alert((res.data&&res.data.error)||'Could not open this inspiration — please try again.');
|
|
616
|
+
})
|
|
617
|
+
.catch(function(){ btn.removeAttribute('data-busy'); btn.innerHTML=label; window.alert('Network error — please try again.'); });
|
|
618
|
+
}
|
|
619
|
+
document.addEventListener('click',function(e){
|
|
620
|
+
var b=e.target&&e.target.closest?e.target.closest('[data-remix]'):null;
|
|
621
|
+
if(!b) return; e.preventDefault(); remixInspiration(b);
|
|
622
|
+
});
|
|
623
|
+
// Click the template-id chip on a feed card to copy the id to the clipboard,
|
|
624
|
+
// with a brief inline "Copied" swap (icon + label) as subtle confirmation.
|
|
625
|
+
function copyText(text){
|
|
626
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){ return navigator.clipboard.writeText(text); }
|
|
627
|
+
return new Promise(function(resolve,reject){
|
|
628
|
+
try{
|
|
629
|
+
var ta=document.createElement('textarea'); ta.value=text;
|
|
630
|
+
ta.style.position='fixed'; ta.style.top='-9999px'; document.body.appendChild(ta);
|
|
631
|
+
ta.focus(); ta.select(); var ok=document.execCommand('copy'); document.body.removeChild(ta);
|
|
632
|
+
ok?resolve():reject(new Error('copy failed'));
|
|
633
|
+
}catch(err){ reject(err); }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
document.addEventListener('click',function(e){
|
|
637
|
+
var chip=e.target&&e.target.closest?e.target.closest('[data-copy-id]'):null;
|
|
638
|
+
if(!chip) return;
|
|
639
|
+
e.preventDefault(); e.stopPropagation();
|
|
640
|
+
var id=chip.getAttribute('data-copy-id')||'';
|
|
641
|
+
if(!id||chip.getAttribute('data-copied')) return;
|
|
642
|
+
copyText(id).then(function(){
|
|
643
|
+
var txt=chip.querySelector('.rk-idchip-text'); var prev=txt?txt.textContent:'';
|
|
644
|
+
chip.setAttribute('data-copied','1'); chip.classList.add('is-copied');
|
|
645
|
+
chip.innerHTML=IDCHECK_ICON+'<span class="rk-idchip-text">Copied</span>';
|
|
646
|
+
window.setTimeout(function(){
|
|
647
|
+
chip.classList.remove('is-copied'); chip.removeAttribute('data-copied');
|
|
648
|
+
chip.innerHTML=COPY_ICON+'<span class="rk-idchip-text">'+esc(prev)+'</span>';
|
|
649
|
+
},1300);
|
|
650
|
+
}).catch(function(){});
|
|
651
|
+
});
|
|
450
652
|
function fmtDuration(s){
|
|
451
653
|
s=Number(s); if(!s||!isFinite(s)||s<=0) return '';
|
|
452
654
|
if(s>=60){ var m=Math.floor(s/60); var r=Math.round(s%60); return m+':'+(r<10?'0':'')+r; }
|
|
@@ -455,6 +657,8 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
455
657
|
var SOURCE_MAP={tiktok:'TikTok','tiktok.com':'TikTok',youtube:'YouTube','youtube.com':'YouTube','youtu.be':'YouTube',instagram:'Instagram','instagram.com':'Instagram',twitter:'Twitter/X',x:'Twitter/X','x.com':'Twitter/X'};
|
|
456
658
|
function sourceLabel(s){ s=String(s||'').toLowerCase(); if(!s) return ''; if(SOURCE_MAP[s]) return SOURCE_MAP[s]; var host=s.replace(/^www\\./,'').split('.')[0]; return host.charAt(0).toUpperCase()+host.slice(1); }
|
|
457
659
|
var EDIT_ICON=${JSON.stringify(EDIT_ICON)};
|
|
660
|
+
var COPY_ICON=${JSON.stringify(COPY_ICON)};
|
|
661
|
+
var IDCHECK_ICON=${JSON.stringify(IDCHECK_ICON)};
|
|
458
662
|
var BOOKMARK_ICON=${JSON.stringify(BOOKMARK_ICON)};
|
|
459
663
|
var SOUND_ON_ICON=${JSON.stringify(SOUND_ON_ICON)};
|
|
460
664
|
var SOUND_OFF_ICON=${JSON.stringify(SOUND_OFF_ICON)};
|
|
@@ -528,32 +732,41 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
528
732
|
// opens the editor via the untouched href). No platform/source pill on the
|
|
529
733
|
// poster (user feedback) — source shows only in the viewer's meta line.
|
|
530
734
|
function posterCardHtml(t, viewIdx){
|
|
531
|
-
var id=t
|
|
735
|
+
var id=domId(t);
|
|
736
|
+
var tid=realTemplateId(t);
|
|
737
|
+
var remix=needsDecompose(t);
|
|
532
738
|
var url=t.previewUrl||t.preview_url||'';
|
|
533
739
|
var hasVideo=Boolean(url)&&!isImage(url);
|
|
534
740
|
var pending=(t.status==='processing'||t.status==='failed');
|
|
535
741
|
var viewable=!pending&&Boolean(url)&&typeof viewIdx==='number'&&viewIdx>=0;
|
|
536
742
|
var dur=fmtDuration(t.durationSeconds);
|
|
537
743
|
var desc=String(t.viralDna||t.summary||'').trim();
|
|
744
|
+
// Real templates deep-link to the editor; inspiration-only cards open the
|
|
745
|
+
// preview viewer instead (a plain anchor href would 404 the editor), and the
|
|
746
|
+
// CTA decomposes on demand.
|
|
747
|
+
var posterHref=remix? '#' : editorUrl(tid);
|
|
538
748
|
var posterInner=mediaHtml(t)
|
|
539
749
|
+(dur?'<span class="rk-discover-dur">'+esc(dur)+'</span>':'')
|
|
540
750
|
+'<div class="rk-discover-scrim"></div>'
|
|
541
751
|
+(hasVideo&&!pending?'<span class="rk-discover-sound" aria-hidden="true"><span class="rk-ds-on">'+SOUND_ON_ICON+'</span><span class="rk-ds-off">'+SOUND_OFF_ICON+'</span></span>':'');
|
|
542
752
|
var poster=pending
|
|
543
753
|
? '<div class="rk-discover-poster">'+posterInner+'</div>'
|
|
544
|
-
: '<a class="rk-discover-poster" href="'+attr(
|
|
754
|
+
: '<a class="rk-discover-poster" href="'+attr(posterHref)+'"'+(viewable?' data-rk-view="'+viewIdx+'"':'')+' aria-label="'+attr((viewable?'Preview ':'Open ')+(t.title||id)+(viewable?'':' in the editor'))+'">'+posterInner+'</a>';
|
|
545
755
|
// bookmark sits NEXT TO the anchor inside the wrap (button-in-anchor is
|
|
546
756
|
// invalid HTML); the wrap is the shared hover context for sound + bookmark
|
|
547
757
|
poster='<div class="rk-discover-posterwrap">'+poster+(pending?'':bmBtnHtml(id,'rk-discover-bm'))+'</div>';
|
|
548
758
|
var cta=pending
|
|
549
759
|
? '<span class="rk-btn rk-btn-ghost rk-btn-sm rk-btn-block rk-discover-fork rk-discover-fork-disabled" aria-disabled="true">'+(t.status==='failed'?'Download failed':'Preparing…')+'</span>'
|
|
550
|
-
:
|
|
760
|
+
: remix
|
|
761
|
+
? '<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" data-remix="'+attr(inspirationIdOf(t))+'">'+EDIT_ICON+' Remix</button>'
|
|
762
|
+
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" href="'+attr(editorUrl(tid))+'">'+EDIT_ICON+' Open Editor</a>';
|
|
551
763
|
return '<article class="rk-discover-card rk-card-hover">'
|
|
552
764
|
+ poster
|
|
553
765
|
+ '<div class="rk-discover-body">'
|
|
554
766
|
+ '<div class="rk-discover-titlewrap">'
|
|
555
767
|
+ '<h3 class="rk-discover-title">'+esc(t.title||id)+'</h3>'
|
|
556
768
|
+ (desc?'<p class="rk-discover-desc">'+esc(desc)+'</p>':'')
|
|
769
|
+
+ (tid?'<button type="button" class="rk-discover-idchip" data-copy-id="'+attr(tid)+'" title="Copy template ID">'+COPY_ICON+'<span class="rk-idchip-text">'+esc(tid)+'</span></button>':'')
|
|
557
770
|
+ '</div>'
|
|
558
771
|
+ cta
|
|
559
772
|
+ '</div>'
|
|
@@ -610,7 +823,9 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
610
823
|
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null; var parkEl=null;
|
|
611
824
|
|
|
612
825
|
function viewerSlideHtml(t,i){
|
|
613
|
-
var id=t
|
|
826
|
+
var id=domId(t);
|
|
827
|
+
var tid=realTemplateId(t);
|
|
828
|
+
var remix=needsDecompose(t);
|
|
614
829
|
var url=t.previewUrl||t.preview_url||'';
|
|
615
830
|
var media=isImage(url)
|
|
616
831
|
? '<img class="rk-discover-viewer-media" src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'">'
|
|
@@ -629,7 +844,9 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
629
844
|
+(meta.length?'<div class="rk-discover-viewer-meta">'+meta.join(' · ')+'</div>':'')
|
|
630
845
|
+'<div class="rk-discover-viewer-title">'+esc(t.title||id)+'</div>'
|
|
631
846
|
+(desc?'<div class="rk-discover-viewer-desc">'+esc(desc)+'</div>':'')
|
|
632
|
-
+
|
|
847
|
+
+(remix
|
|
848
|
+
? '<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" data-remix="'+attr(inspirationIdOf(t))+'">'+EDIT_ICON+' Remix</button>'
|
|
849
|
+
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" href="'+attr(editorUrl(tid))+'">'+EDIT_ICON+' Open Editor</a>')
|
|
633
850
|
+'</div>'
|
|
634
851
|
+'</figure>'
|
|
635
852
|
+'</section>';
|
|
@@ -764,9 +981,18 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
764
981
|
if(feedRoot){
|
|
765
982
|
var gridEl=feedRoot.querySelector('[data-rk-grid]');
|
|
766
983
|
var stateEl=feedRoot.querySelector('[data-rk-state]');
|
|
767
|
-
// search
|
|
768
|
-
|
|
984
|
+
// search lives in the shared toolbar row (outside [data-rk-feed]); the
|
|
985
|
+
// visible-count is folded into the search placeholder rather than shown as
|
|
986
|
+
// a separate label.
|
|
769
987
|
var searchEl=root.querySelector('[data-rk-search]');
|
|
988
|
+
// caption the placeholder with the count, but only while the box is empty —
|
|
989
|
+
// a typed query keeps its own placeholder-less state.
|
|
990
|
+
function setCountHint(n){
|
|
991
|
+
if(!searchEl) return;
|
|
992
|
+
searchEl.placeholder = (n==null)
|
|
993
|
+
? 'Search templates'
|
|
994
|
+
: 'Search '+n+' template'+(n===1?'':'s');
|
|
995
|
+
}
|
|
770
996
|
var bmChip=root.querySelector('[data-rk-bmfilter]');
|
|
771
997
|
var bmCountEl=root.querySelector('[data-rk-bmcount]');
|
|
772
998
|
var bmFilter=false;
|
|
@@ -776,7 +1002,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
776
1002
|
function feedSetState(msg,isError){
|
|
777
1003
|
if(gridEl) gridEl.innerHTML='';
|
|
778
1004
|
if(stateEl){ stateEl.hidden=false; stateEl.innerHTML=msg; stateEl.classList.toggle('is-error',!!isError); }
|
|
779
|
-
|
|
1005
|
+
setCountHint(null);
|
|
780
1006
|
}
|
|
781
1007
|
function updateBmChip(){
|
|
782
1008
|
if(!bmChip) return;
|
|
@@ -798,7 +1024,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
798
1024
|
}
|
|
799
1025
|
if(stateEl) stateEl.hidden=true;
|
|
800
1026
|
if(gridEl) gridEl.innerHTML=vis.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
801
|
-
|
|
1027
|
+
setCountHint(vis.length);
|
|
802
1028
|
}
|
|
803
1029
|
if(bmChip) bmChip.addEventListener('click',function(){ bmFilter=!bmFilter; feedRender(); });
|
|
804
1030
|
bmChangeHandlers.push(function(){
|
|
@@ -807,17 +1033,42 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
807
1033
|
// re-render restarts muted autoplay, so re-park it if the viewer is open
|
|
808
1034
|
if(bmFilter){ feedRender(); if(viewerCtl&&viewerCtl.isOpen()) parkVideos(gridEl); }
|
|
809
1035
|
});
|
|
810
|
-
function
|
|
811
|
-
|
|
812
|
-
feedState.loading=
|
|
813
|
-
|
|
1036
|
+
function applyFeedItems(items){
|
|
1037
|
+
feedState.items=Array.isArray(items)?items:[];
|
|
1038
|
+
feedState.loading=false;
|
|
1039
|
+
feedRender();
|
|
1040
|
+
}
|
|
1041
|
+
// Direct same-origin fetch — the fallback for when the Zustand bridge isn't
|
|
1042
|
+
// on the page yet (its deferred module is still loading) or is disabled.
|
|
1043
|
+
function feedFetchDirect(query,my){
|
|
814
1044
|
var url=feedEndpoint;
|
|
815
1045
|
try{ var u=new URL(feedEndpoint, window.location.href); if(query) u.searchParams.set('q', query); if(accountId) u.searchParams.set('account', accountId); url=u.toString(); }catch(e){ url=feedEndpoint; }
|
|
816
1046
|
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
817
1047
|
.then(function(r){ if(!r.ok){ throw new Error(r.status===401?'Sign in to browse the template feed.':("Couldn't load templates ("+r.status+").")); } return r.json(); })
|
|
818
|
-
.then(function(j){ if(my!==feedReq) return;
|
|
1048
|
+
.then(function(j){ if(my!==feedReq) return; applyFeedItems((j&&Array.isArray(j.templates))?j.templates:[]); })
|
|
819
1049
|
.catch(function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); });
|
|
820
1050
|
}
|
|
1051
|
+
// Feed loads flow through the Zustand store bridge (SSR-seed + SWR cache);
|
|
1052
|
+
// it may call onResult twice for the default feed (cached snapshot first,
|
|
1053
|
+
// then a background-revalidated fresh copy) — both are honored under the
|
|
1054
|
+
// same request token so a newer search still wins.
|
|
1055
|
+
function feedLoad(query){
|
|
1056
|
+
var my=++feedReq;
|
|
1057
|
+
feedState.loading=true;
|
|
1058
|
+
// Only flash the loading state when nothing's painted yet — a
|
|
1059
|
+
// revalidate must never blank an already-good grid.
|
|
1060
|
+
if(!feedState.items.length) feedSetState('Loading templates…',false);
|
|
1061
|
+
var bridge=window.__vidfarmDiscover;
|
|
1062
|
+
if(bridge&&bridge.loadFeed){
|
|
1063
|
+
bridge.loadFeed(query,{
|
|
1064
|
+
onLoading:function(){ if(my===feedReq && !feedState.items.length) feedSetState('Loading templates…',false); },
|
|
1065
|
+
onResult:function(items){ if(my!==feedReq) return; applyFeedItems(items); },
|
|
1066
|
+
onError:function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); }
|
|
1067
|
+
});
|
|
1068
|
+
} else {
|
|
1069
|
+
feedFetchDirect(query,my);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
821
1072
|
// Honor a ?q= (e.g. from a category link) in the page URL.
|
|
822
1073
|
var initialQuery='';
|
|
823
1074
|
try{ initialQuery=(new URL(window.location.href)).searchParams.get('q')||''; }catch(e){ initialQuery=''; }
|
|
@@ -835,7 +1086,140 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
835
1086
|
// plain click on a card → shared viewer over the CURRENT visible feed
|
|
836
1087
|
// (search + bookmark filter applied)
|
|
837
1088
|
bindViewerOpens(gridEl, function(){ return feedState.visible; });
|
|
838
|
-
|
|
1089
|
+
// SSR-seed: if the server inlined the first page of the default feed AND
|
|
1090
|
+
// there's no ?q= search, paint it immediately with NO initial fetch — this
|
|
1091
|
+
// is what kills the redundant SSR-render → client-fetch round trip.
|
|
1092
|
+
var seeded=false;
|
|
1093
|
+
if(!initialQuery && boot.feedInitial && Array.isArray(boot.feedInitial) && boot.feedInitial.length){
|
|
1094
|
+
applyFeedItems(boot.feedInitial);
|
|
1095
|
+
seeded=true;
|
|
1096
|
+
}
|
|
1097
|
+
if(!seeded){
|
|
1098
|
+
feedLoad(initialQuery.trim());
|
|
1099
|
+
}
|
|
1100
|
+
// A background revalidation (stale snapshot / BFCache restore) from the
|
|
1101
|
+
// store bridge re-paints the grid — but only while not mid-search.
|
|
1102
|
+
window.addEventListener('vidfarm:discover-feed-updated',function(ev){
|
|
1103
|
+
var q=searchEl?searchEl.value.trim():'';
|
|
1104
|
+
if(q) return;
|
|
1105
|
+
var items=ev&&ev.detail&&ev.detail.items;
|
|
1106
|
+
if(Array.isArray(items)) applyFeedItems(items);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// ── "Add New" modal — paste a social URL or upload a video, add it to the
|
|
1110
|
+
// caller's private feed (real POST /discover/templates + upload presign
|
|
1111
|
+
// pair), then reload the feed so the new pending card appears. In feed
|
|
1112
|
+
// scope so it can reach feedLoad + the current search value.
|
|
1113
|
+
var addModal=root.querySelector('[data-rk-add-modal]');
|
|
1114
|
+
var addBtn=root.querySelector('[data-rk-add]');
|
|
1115
|
+
if(addModal&&addBtn){
|
|
1116
|
+
var addForm=addModal.querySelector('[data-rk-add-form]');
|
|
1117
|
+
var addUrl=addModal.querySelector('[data-rk-add-url]');
|
|
1118
|
+
var addFile=addModal.querySelector('[data-rk-add-file]');
|
|
1119
|
+
var addFileLabel=addModal.querySelector('[data-rk-add-filelabel]');
|
|
1120
|
+
var addFileText=addModal.querySelector('[data-rk-add-filetext]');
|
|
1121
|
+
var addTagline=addModal.querySelector('[data-rk-add-tagline]');
|
|
1122
|
+
var addNotes=addModal.querySelector('[data-rk-add-notes]');
|
|
1123
|
+
var addErr=addModal.querySelector('[data-rk-add-error]');
|
|
1124
|
+
var addSubmit=addModal.querySelector('[data-rk-add-submit]');
|
|
1125
|
+
var MAX_UPLOAD=200*1024*1024;
|
|
1126
|
+
var defaultFileText=addFileText?addFileText.innerHTML:'';
|
|
1127
|
+
var addBusy=false;
|
|
1128
|
+
|
|
1129
|
+
function showAddErr(msg){ if(addErr){ addErr.textContent=msg||''; addErr.hidden=!msg; } }
|
|
1130
|
+
function openAdd(){
|
|
1131
|
+
addModal.hidden=false;
|
|
1132
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
1133
|
+
if(addUrl){ try{ addUrl.focus(); }catch(e){} }
|
|
1134
|
+
}
|
|
1135
|
+
function closeAdd(){
|
|
1136
|
+
if(addBusy) return;
|
|
1137
|
+
addModal.hidden=true;
|
|
1138
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
1139
|
+
}
|
|
1140
|
+
function resetAdd(){
|
|
1141
|
+
if(addForm) addForm.reset();
|
|
1142
|
+
if(addFileLabel) addFileLabel.classList.remove('is-picked');
|
|
1143
|
+
if(addFileText) addFileText.innerHTML=defaultFileText;
|
|
1144
|
+
showAddErr('');
|
|
1145
|
+
}
|
|
1146
|
+
function setAddBusy(on,label){
|
|
1147
|
+
addBusy=on;
|
|
1148
|
+
if(addSubmit){ addSubmit.disabled=on; addSubmit.textContent=on?(label||'Adding\\u2026'):'Add to feed'; }
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
addBtn.addEventListener('click',openAdd);
|
|
1152
|
+
addModal.querySelectorAll('[data-rk-add-close]').forEach(function(b){ b.addEventListener('click',closeAdd); });
|
|
1153
|
+
document.addEventListener('keydown',function(e){ if(e.key==='Escape'&&!addModal.hidden) closeAdd(); });
|
|
1154
|
+
|
|
1155
|
+
if(addFile){
|
|
1156
|
+
addFile.addEventListener('change',function(){
|
|
1157
|
+
var f=addFile.files&&addFile.files[0];
|
|
1158
|
+
if(addFileLabel) addFileLabel.classList.toggle('is-picked',!!f);
|
|
1159
|
+
if(addFileText) addFileText.innerHTML=f?esc(f.name):defaultFileText;
|
|
1160
|
+
showAddErr('');
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function ingest(uploadRef){
|
|
1165
|
+
var tagline=addTagline?addTagline.value.trim():'';
|
|
1166
|
+
var notes=addNotes?addNotes.value.trim():'';
|
|
1167
|
+
var payload=uploadRef
|
|
1168
|
+
? { upload:uploadRef, tagline:tagline||null, notes:notes||null }
|
|
1169
|
+
: { source_url:(addUrl?addUrl.value.trim():''), tagline:tagline||null, notes:notes||null };
|
|
1170
|
+
return fetch(withAccount('/discover/templates'),{
|
|
1171
|
+
method:'POST',credentials:'same-origin',
|
|
1172
|
+
headers:{'content-type':'application/json',Accept:'application/json'},
|
|
1173
|
+
body:JSON.stringify(payload)
|
|
1174
|
+
}).then(function(r){ return r.json().catch(function(){ return {}; }).then(function(d){ return {ok:r.ok,status:r.status,data:d}; }); });
|
|
1175
|
+
}
|
|
1176
|
+
function uploadFile(file){
|
|
1177
|
+
return fetch(withAccount('/discover/templates/upload/presign'),{
|
|
1178
|
+
method:'POST',credentials:'same-origin',
|
|
1179
|
+
headers:{'content-type':'application/json',Accept:'application/json'},
|
|
1180
|
+
body:JSON.stringify({ file_name:file.name, content_type:file.type||null, size_bytes:file.size })
|
|
1181
|
+
}).then(function(r){ return r.json().catch(function(){ return {}; }).then(function(d){ return {ok:r.ok,data:d}; }); })
|
|
1182
|
+
.then(function(res){
|
|
1183
|
+
if(!res.ok) throw new Error((res.data&&res.data.error)||'Unable to start the video upload.');
|
|
1184
|
+
var p=res.data||{};
|
|
1185
|
+
if(p.transport==='presigned'&&p.upload&&p.upload.url&&p.storage_key){
|
|
1186
|
+
return fetch(p.upload.url,{ method:(p.upload.method||'PUT'), headers:(p.upload.headers||{}), body:file })
|
|
1187
|
+
.then(function(put){ if(!put.ok) throw new Error('Video upload failed ('+put.status+'). Try again.');
|
|
1188
|
+
return { storage_key:p.storage_key, file_name:p.file_name||file.name }; });
|
|
1189
|
+
}
|
|
1190
|
+
var form=new FormData(); form.append('file',file,p.file_name||file.name);
|
|
1191
|
+
return fetch((p.upload&&p.upload.url)||'/discover/templates/upload',{ method:'POST',credentials:'same-origin',body:form })
|
|
1192
|
+
.then(function(direct){ return direct.json().catch(function(){ return {}; }).then(function(dd){
|
|
1193
|
+
if(!direct.ok||!dd.storage_key) throw new Error(dd.error||'Video upload failed. Try again.');
|
|
1194
|
+
return { storage_key:dd.storage_key, file_name:dd.file_name||file.name }; }); });
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if(addForm){
|
|
1199
|
+
addForm.addEventListener('submit',function(e){
|
|
1200
|
+
e.preventDefault();
|
|
1201
|
+
if(addBusy) return;
|
|
1202
|
+
showAddErr('');
|
|
1203
|
+
var file=addFile&&addFile.files?addFile.files[0]:null;
|
|
1204
|
+
var url=addUrl?addUrl.value.trim():'';
|
|
1205
|
+
if(!file&&!url){ showAddErr('Paste a TikTok, YouTube, Twitter/X, or Instagram video URL \\u2014 or upload a video file.'); return; }
|
|
1206
|
+
if(file&&file.size>MAX_UPLOAD){ showAddErr('Video uploads can be at most 200 MB.'); return; }
|
|
1207
|
+
setAddBusy(true,file?'Uploading\\u2026':'Adding\\u2026');
|
|
1208
|
+
var chain=file?uploadFile(file):Promise.resolve(null);
|
|
1209
|
+
chain.then(function(uploadRef){ return ingest(uploadRef); })
|
|
1210
|
+
.then(function(res){
|
|
1211
|
+
setAddBusy(false);
|
|
1212
|
+
if(!res.ok){ showAddErr((res.data&&res.data.error)||'Unable to add that video.'); return; }
|
|
1213
|
+
resetAdd(); closeAdd();
|
|
1214
|
+
// A new inspiration just landed — drop the cached snapshot so the
|
|
1215
|
+
// reload fetches fresh (no stale-cache flash of the old feed).
|
|
1216
|
+
if(window.__vidfarmDiscover&&window.__vidfarmDiscover.invalidate) window.__vidfarmDiscover.invalidate();
|
|
1217
|
+
feedLoad(searchEl?searchEl.value.trim():'');
|
|
1218
|
+
})
|
|
1219
|
+
.catch(function(err){ setAddBusy(false); showAddErr((err&&err.message)?err.message:'Unable to add that video.'); });
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
839
1223
|
}
|
|
840
1224
|
|
|
841
1225
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -849,9 +1233,32 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
849
1233
|
var hint=swipeRoot.querySelector('[data-actions-hint]');
|
|
850
1234
|
var skipBtn=swipeRoot.querySelector('[data-swipe-skip]');
|
|
851
1235
|
var forkBtn=swipeRoot.querySelector('[data-swipe-fork]');
|
|
852
|
-
var
|
|
1236
|
+
var soundBtn=swipeRoot.querySelector('[data-swipe-sound]');
|
|
1237
|
+
var sw={target:5,customizing:0,productSet:false,busy:false,refilling:false,loaded:false,soundOn:false,known:{}};
|
|
853
1238
|
var nodes=[]; // live card DOM nodes, top-of-deck first
|
|
854
1239
|
|
|
1240
|
+
// ── audio: the same-origin preview iframes poll these window flags (see
|
|
1241
|
+
// injectSwipePreviewFit). Only the TOP card's id is named audible so the
|
|
1242
|
+
// preloaded 2nd iframe stays silent (audio never doubles). Deck previews
|
|
1243
|
+
// always start muted (the only autoplay browsers permit); the toggle is the
|
|
1244
|
+
// user gesture that unlocks sound.
|
|
1245
|
+
function syncSound(){
|
|
1246
|
+
try{
|
|
1247
|
+
window.__VF_SWIPE_SOUND__ = sw.soundOn===true;
|
|
1248
|
+
window.__VF_SWIPE_SOUND_TOP__ = nodes.length? String(nodes[0]._card.id): null;
|
|
1249
|
+
}catch(e){}
|
|
1250
|
+
}
|
|
1251
|
+
function updateSoundBtn(){
|
|
1252
|
+
if(!soundBtn) return;
|
|
1253
|
+
var show=nodes.length>0;
|
|
1254
|
+
soundBtn.hidden=!show;
|
|
1255
|
+
soundBtn.classList.toggle('is-on', sw.soundOn);
|
|
1256
|
+
soundBtn.setAttribute('aria-pressed', sw.soundOn?'true':'false');
|
|
1257
|
+
soundBtn.setAttribute('aria-label', sw.soundOn?'Mute':'Unmute');
|
|
1258
|
+
soundBtn.innerHTML = sw.soundOn? SOUND_ON_ICON : SOUND_OFF_ICON;
|
|
1259
|
+
}
|
|
1260
|
+
if(soundBtn) soundBtn.addEventListener('click',function(){ sw.soundOn=!sw.soundOn; syncSound(); updateSoundBtn(); });
|
|
1261
|
+
|
|
855
1262
|
function swShowState(html,kind){
|
|
856
1263
|
if(swStateEl){ swStateEl.hidden=false; swStateEl.innerHTML=html; swStateEl.classList.toggle('is-error',kind==='error'); }
|
|
857
1264
|
actionBtns.forEach(function(b){ b.hidden=true; });
|
|
@@ -860,6 +1267,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
860
1267
|
function swHideState(){ if(swStateEl) swStateEl.hidden=true; }
|
|
861
1268
|
|
|
862
1269
|
function makeCard(card){
|
|
1270
|
+
if(card&&card.id) sw.known[String(card.id)]=true;
|
|
863
1271
|
var el=document.createElement('article');
|
|
864
1272
|
el.className='rk-discover-swipe';
|
|
865
1273
|
el._card=card; el._iframed=false;
|
|
@@ -883,6 +1291,13 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
883
1291
|
frame.setAttribute('loading','lazy');
|
|
884
1292
|
frame.setAttribute('scrolling','no');
|
|
885
1293
|
frame.setAttribute('allow','autoplay');
|
|
1294
|
+
// The fallback is an OPAQUE dark placeholder that sits later in the DOM,
|
|
1295
|
+
// so it paints over the iframe. Reveal the live preview once it loads by
|
|
1296
|
+
// fading the placeholder out (kept as a load state until then). A timed
|
|
1297
|
+
// fallback covers the case where 'load' never fires.
|
|
1298
|
+
function revealFrame(){ if(fb){ fb.style.opacity='0'; fb.style.pointerEvents='none'; } }
|
|
1299
|
+
frame.addEventListener('load', function(){ window.setTimeout(revealFrame, 120); });
|
|
1300
|
+
window.setTimeout(revealFrame, 2600);
|
|
886
1301
|
if(fb&&fb.parentNode) fb.parentNode.insertBefore(frame, fb); else el.insertBefore(frame, el.firstChild);
|
|
887
1302
|
el._iframed=true;
|
|
888
1303
|
}
|
|
@@ -898,6 +1313,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
898
1313
|
var has=nodes.length>0;
|
|
899
1314
|
actionBtns.forEach(function(b){ b.hidden=!has; });
|
|
900
1315
|
if(hint) hint.hidden=!has;
|
|
1316
|
+
syncSound(); updateSoundBtn();
|
|
901
1317
|
if(!has) swCaughtUp();
|
|
902
1318
|
}
|
|
903
1319
|
function swCaughtUp(){
|
|
@@ -935,8 +1351,43 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
935
1351
|
if(j.filled&&j.record){ makeCard(j.record); restack(); }
|
|
936
1352
|
var more=j.filled&&nodes.length<sw.target&&j.reason!=='no_eligible'&&j.reason!=='at_target';
|
|
937
1353
|
if(more) refill();
|
|
1354
|
+
// Server said it's still working (no card yet, not "done") — the
|
|
1355
|
+
// customization keeps running Lambda-side even if THIS request replies
|
|
1356
|
+
// late; poll the stack so a finished card appears without a refresh.
|
|
1357
|
+
else if(!j.filled&&j.reason!=='no_eligible'&&j.reason!=='at_target') schedulePoll();
|
|
1358
|
+
})
|
|
1359
|
+
.catch(function(){
|
|
1360
|
+
// Timeout / 504 / network drop: customizeOneMore often runs past the
|
|
1361
|
+
// gateway's reply window, so the card lands server-side after we gave
|
|
1362
|
+
// up here. Poll the stack to pick it up (this is the "504 on first
|
|
1363
|
+
// load, works after a manual refresh" case — now it self-heals).
|
|
1364
|
+
sw.refilling=false; schedulePoll();
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
// ── stack re-poll: recover cards the refill Lambda produced after its HTTP
|
|
1368
|
+
// reply timed out. Backs off after a fixed number of empty tries so we never
|
|
1369
|
+
// poll forever.
|
|
1370
|
+
var pollTimer=null, pollTries=0;
|
|
1371
|
+
function schedulePoll(){
|
|
1372
|
+
if(pollTimer||pollTries>=12) return;
|
|
1373
|
+
if(nodes.length>=sw.target) return;
|
|
1374
|
+
pollTimer=window.setTimeout(function(){ pollTimer=null; pollStack(); }, 5000);
|
|
1375
|
+
}
|
|
1376
|
+
function pollStack(){
|
|
1377
|
+
if(nodes.length>=sw.target) return;
|
|
1378
|
+
fetch(withAccount(swipeStackEndpoint),{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1379
|
+
.then(function(r){ return r.ok? r.json(): null; })
|
|
1380
|
+
.then(function(j){
|
|
1381
|
+
if(!j){ pollTries++; schedulePoll(); return; }
|
|
1382
|
+
sw.customizing=Number(j&&j.customizing)||0;
|
|
1383
|
+
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
1384
|
+
var added=0;
|
|
1385
|
+
stack.forEach(function(card){ if(card&&card.id&&!sw.known[String(card.id)]){ nodes.push(makeCard(card)); added++; } });
|
|
1386
|
+
if(added){ pollTries=0; swHideState(); restack(); }
|
|
1387
|
+
else { pollTries++; if(!nodes.length) swCaughtUp(); }
|
|
1388
|
+
if((sw.customizing>0||nodes.length<sw.target)&&nodes.length<sw.target) schedulePoll();
|
|
938
1389
|
})
|
|
939
|
-
.catch(function(){
|
|
1390
|
+
.catch(function(){ pollTries++; schedulePoll(); });
|
|
940
1391
|
}
|
|
941
1392
|
function loadStack(){
|
|
942
1393
|
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div>Loading your deck…</div>');
|
|
@@ -953,7 +1404,7 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
953
1404
|
sw.productSet=Boolean(j&&j.product_description_set);
|
|
954
1405
|
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
955
1406
|
if(!sw.productSet){
|
|
956
|
-
swShowState('<div class="rk-discover-swipe-state-title">Tell us about your product</div><div>Swipe auto-recuts viral templates into ads for your product. Add a product / offer description in Settings to start the deck.</div><a class="rk-btn rk-btn-gold rk-btn-sm" href="/settings
|
|
1407
|
+
swShowState('<div class="rk-discover-swipe-state-title">Tell us about your product</div><div>Swipe auto-recuts viral templates into ads for your product. Add a product / offer description in Settings to start the deck.</div><a class="rk-btn rk-btn-gold rk-btn-sm" href="/settings/profile">Open Settings</a>');
|
|
957
1408
|
return;
|
|
958
1409
|
}
|
|
959
1410
|
nodes=stack.map(makeCard);
|
|
@@ -1088,9 +1539,13 @@ video.rk-discover-viewer-media{cursor:pointer}
|
|
|
1088
1539
|
title: `vidfarm reskin — Discover (${mode})`,
|
|
1089
1540
|
description: "Reskinned vidfarm Discover — real Feed / Swipe / Categories, live template posters, warm sunny SaaS.",
|
|
1090
1541
|
activeSlug: "discover",
|
|
1542
|
+
account: { name: input.name, email: input.email },
|
|
1091
1543
|
pageCss,
|
|
1092
1544
|
body,
|
|
1093
|
-
script
|
|
1545
|
+
script,
|
|
1546
|
+
// Feed mode hydrates a Zustand store (SSR-seed + stale-while-revalidate);
|
|
1547
|
+
// the other modes don't need it.
|
|
1548
|
+
moduleScripts: mode === "feed" ? ["/assets/discover-client-app.js"] : undefined
|
|
1094
1549
|
});
|
|
1095
1550
|
}
|
|
1096
1551
|
//# sourceMappingURL=discover-page.js.map
|