@mevdragon/vidfarm-devcli 0.17.0 → 0.18.0
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 +3 -3
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/app.js +301 -45
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +502 -187
- package/dist/src/reskin/chat-page.js +470 -277
- package/dist/src/reskin/discover-page.js +988 -272
- package/dist/src/reskin/document.js +602 -13
- package/dist/src/reskin/help-page.js +138 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +435 -262
- package/dist/src/reskin/login-page.js +118 -81
- package/dist/src/reskin/pricing-page.js +195 -166
- package/dist/src/reskin/settings-page.js +520 -186
- package/dist/src/reskin/theme.js +106 -9
- package/package.json +1 -1
|
@@ -1,17 +1,56 @@
|
|
|
1
|
-
// /reskin/discover — the template discovery surface,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// • categories → /reskin/discover/categories — browse-by-format cards
|
|
1
|
+
// /reskin/discover — the template discovery surface, MIGRATED to the REAL
|
|
2
|
+
// production feed (part of the /reskin/* UX migration; reference:
|
|
3
|
+
// job-runs-page.ts for the "render a shell → client-fetch a real endpoint"
|
|
4
|
+
// pattern, settings-page.ts for the real-data render pattern).
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
// Categories segmented toggle
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
6
|
+
// Three server-routed modes, each its own URL, surfaced as a Feed / Swipe /
|
|
7
|
+
// Categories segmented toggle (three real <a> links):
|
|
8
|
+
// • feed → /reskin/discover/feed — real /discover/feed poster grid
|
|
9
|
+
// • swipe → /reskin/discover/swipe — real /discover/swipe/* Tinder deck
|
|
10
|
+
// • categories → /reskin/discover/categories — real /discover/popular groups
|
|
11
|
+
//
|
|
12
|
+
// Every mode renders the farmville shell + a self-contained module script that
|
|
13
|
+
// fetches the SAME real, same-origin endpoints the live React SPA hits, with
|
|
14
|
+
// credentials, and renders real poster cards (real title, real thumbnail/video
|
|
15
|
+
// from the real url, real source + duration) that fork/open into the real
|
|
16
|
+
// editor at /editor/<template_id>. Loading / empty / error states are all
|
|
17
|
+
// guarded so an offline fetch degrades to a friendly state instead of throwing.
|
|
18
|
+
//
|
|
19
|
+
// Poster-grid extras (feed + categories reveal, which share posterCardHtml):
|
|
20
|
+
// • hover audio — mousing over a card's (muted, autoplaying) video unmutes
|
|
21
|
+
// it while the pointer stays, with an autoplay-policy fallback that
|
|
22
|
+
// reverts to muted playback if the browser blocks unmuted play.
|
|
23
|
+
// • TikTok-style viewer — clicking a card opens a fullscreen vertical
|
|
24
|
+
// scroll-snap deck (swipe / wheel / arrow keys between templates, sound
|
|
25
|
+
// on, tap video to pause, mute toggle) with an "Open Editor" CTA per
|
|
26
|
+
// slide. The card's "Open Editor" button still deep-links the editor.
|
|
27
|
+
// The viewer is shared: feed swipes the (filtered) feed, categories
|
|
28
|
+
// swipes the revealed group.
|
|
29
|
+
// • bookmarks — one-tap bookmark on each viewer slide + on cards on hover,
|
|
30
|
+
// persisted client-side in localStorage ('rk-discover-bookmarks'; there
|
|
31
|
+
// is no backend favorite endpoint), plus a "bookmarked only" toolbar
|
|
32
|
+
// filter chip on the feed so saved templates are findable.
|
|
33
|
+
// • no platform pill on posters (user feedback) — source only shows as the
|
|
34
|
+
// small meta line inside the viewer slide.
|
|
35
|
+
//
|
|
36
|
+
// The renderer keeps its `mode` signature (so the existing zero-arg
|
|
37
|
+
// `/reskin/discover/:mode` route keeps working — the client fetches the live
|
|
38
|
+
// endpoints same-origin using the browser session cookie) and adds an optional
|
|
39
|
+
// 2nd `input` arg (defaulted) so the parent route can, if it wants, pass the
|
|
40
|
+
// signed-in account id (→ ?account= on fetches + editor links, matching the
|
|
41
|
+
// live editorHref) and override any endpoint.
|
|
42
|
+
import { reskinDocument, rkEscape } from "./document.js";
|
|
43
|
+
import { POPULAR_INSPIRATION_GROUPS } from "../frontend/homepage-shared.js";
|
|
44
|
+
const DEFAULT_INPUT = {
|
|
45
|
+
accountId: "",
|
|
46
|
+
isLoggedIn: false,
|
|
47
|
+
feedEndpoint: "/discover/feed",
|
|
48
|
+
popularEndpoint: "/discover/popular",
|
|
49
|
+
swipeStackEndpoint: "/discover/swipe/stack",
|
|
50
|
+
swipeRefillEndpoint: "/discover/swipe/refill",
|
|
51
|
+
swipeBase: "/discover/swipe",
|
|
52
|
+
editorBase: "/editor"
|
|
53
|
+
};
|
|
15
54
|
const MODES = [
|
|
16
55
|
{ mode: "feed", label: "Feed" },
|
|
17
56
|
{ mode: "swipe", label: "Swipe" },
|
|
@@ -19,227 +58,166 @@ const MODES = [
|
|
|
19
58
|
];
|
|
20
59
|
const MODE_COPY = {
|
|
21
60
|
feed: {
|
|
22
|
-
|
|
23
|
-
sub: "Browse proven short-form formats, then fork one into a live editor and make it yours in an afternoon."
|
|
61
|
+
sub: "Browse proven short-form formats, then open one in the editor and make it yours in an afternoon."
|
|
24
62
|
},
|
|
25
63
|
swipe: {
|
|
26
|
-
badge: "Swipe to customize",
|
|
27
64
|
sub: "One template at a time — keep the ones that fit and we'll recut them into ads for your brand."
|
|
28
65
|
},
|
|
29
66
|
categories: {
|
|
30
|
-
badge: "Browse by format",
|
|
31
67
|
sub: "Jump straight to the style you need — every short-form format, neatly grouped."
|
|
32
68
|
}
|
|
33
69
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
CARDS[2], // Kinetic explainer
|
|
53
|
-
CARDS[7], // Reddit story
|
|
54
|
-
CARDS[1] // Talking-head monologue
|
|
55
|
-
];
|
|
56
|
-
const CATEGORIES = [
|
|
57
|
-
{
|
|
58
|
-
slug: "ugc", label: "UGC ad", emoji: "💄", tile: "rk-tile-coral", count: 128,
|
|
59
|
-
blurb: "Creator-style ads that feel like a recommendation, not an ad.",
|
|
60
|
-
thumbs: [
|
|
61
|
-
{ emoji: "💄", grad: "linear-gradient(150deg,#ffd76a,#f97316)" },
|
|
62
|
-
{ emoji: "💬", grad: "linear-gradient(160deg,#5eead4,#0d9488)" },
|
|
63
|
-
{ emoji: "🧴", grad: "linear-gradient(150deg,#ffc9d4,#f472b6)" },
|
|
64
|
-
{ emoji: "🛍️", grad: "linear-gradient(155deg,#ffb27a,#f43f5e)" }
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
slug: "talking", label: "Talking head", emoji: "🎙️", tile: "rk-tile-sky", count: 96,
|
|
69
|
-
blurb: "Straight-to-camera monologues with punchy captioned hooks.",
|
|
70
|
-
thumbs: [
|
|
71
|
-
{ emoji: "🎙️", grad: "linear-gradient(160deg,#6cc0f5,#3f9fe0)" },
|
|
72
|
-
{ emoji: "✨", grad: "linear-gradient(155deg,#ffc9d4,#f472b6)" },
|
|
73
|
-
{ emoji: "🗣️", grad: "linear-gradient(160deg,#8fa3ff,#4f46e5)" }
|
|
74
|
-
]
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
slug: "explainer", label: "Explainer", emoji: "🧠", tile: "rk-tile-violet", count: 74,
|
|
78
|
-
blurb: "Kinetic typography and diagrams that make an idea click.",
|
|
79
|
-
thumbs: [
|
|
80
|
-
{ emoji: "🧠", grad: "linear-gradient(155deg,#c4a0ee,#7c3aed)" },
|
|
81
|
-
{ emoji: "📊", grad: "linear-gradient(150deg,#ffe08a,#38bdf8)" },
|
|
82
|
-
{ emoji: "🔗", grad: "linear-gradient(160deg,#a5b4fc,#6366f1)" },
|
|
83
|
-
{ emoji: "💡", grad: "linear-gradient(150deg,#ffe08a,#f59e0b)" }
|
|
84
|
-
]
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
slug: "listicle", label: "Listicle", emoji: "🔟", tile: "rk-tile-green", count: 63,
|
|
88
|
-
blurb: "Countdowns, tier lists and ranked picks that keep watch time high.",
|
|
89
|
-
thumbs: [
|
|
90
|
-
{ emoji: "🔟", grad: "linear-gradient(160deg,#7de0a3,#16a34a)" },
|
|
91
|
-
{ emoji: "🏆", grad: "linear-gradient(150deg,#ffd76a,#fb7185)" },
|
|
92
|
-
{ emoji: "⭐", grad: "linear-gradient(150deg,#ffe08a,#f97316)" }
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
slug: "meme", label: "Meme", emoji: "😂", tile: "rk-tile-gold", count: 141,
|
|
97
|
-
blurb: "Recaptioned reactions and relatable brainrot for reach.",
|
|
98
|
-
thumbs: [
|
|
99
|
-
{ emoji: "😂", grad: "linear-gradient(150deg,#ffb27a,#f43f5e)" },
|
|
100
|
-
{ emoji: "🤡", grad: "linear-gradient(150deg,#ffd76a,#f97316)" },
|
|
101
|
-
{ emoji: "🔥", grad: "linear-gradient(150deg,#fb923c,#ef4444)" },
|
|
102
|
-
{ emoji: "💀", grad: "linear-gradient(160deg,#94a3b8,#334155)" }
|
|
103
|
-
]
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
slug: "product", label: "Product demo", emoji: "📱", tile: "rk-tile-ink", count: 52,
|
|
107
|
-
blurb: "Screen-first walkthroughs that sell the feature in 30 seconds.",
|
|
108
|
-
thumbs: [
|
|
109
|
-
{ emoji: "📱", grad: "linear-gradient(155deg,#4b4b4b,#171717)" },
|
|
110
|
-
{ emoji: "🖥️", grad: "linear-gradient(160deg,#64748b,#0f172a)" },
|
|
111
|
-
{ emoji: "⚡", grad: "linear-gradient(150deg,#ffe08a,#38bdf8)" }
|
|
112
|
-
]
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
slug: "story", label: "Story", emoji: "📖", tile: "rk-tile-violet", count: 88,
|
|
116
|
-
blurb: "Narrated stories over satisfying gameplay or B-roll.",
|
|
117
|
-
thumbs: [
|
|
118
|
-
{ emoji: "📖", grad: "linear-gradient(160deg,#8fa3ff,#4f46e5)" },
|
|
119
|
-
{ emoji: "🎬", grad: "linear-gradient(160deg,#c084fc,#1e1b4b)" },
|
|
120
|
-
{ emoji: "🎮", grad: "linear-gradient(160deg,#5eead4,#0d9488)" },
|
|
121
|
-
{ emoji: "📜", grad: "linear-gradient(150deg,#ffd76a,#f59e0b)" }
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
];
|
|
125
|
-
const FORK_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"><circle cx="6" cy="6" r="2.4"/><circle cx="6" cy="18" r="2.4"/><circle cx="18" cy="7" r="2.4"/><path d="M6 8.4v7.2M18 9.4c0 3-3 3.6-6 4.2"/></svg>`;
|
|
126
|
-
// ── feed: poster card ──────────────────────────────────────────────────────
|
|
127
|
-
// Deliberately calm: just the duration overlay on the poster, ONE plain muted
|
|
128
|
-
// meta line (handle · forks — not pills), and the single gold Fork CTA.
|
|
129
|
-
function posterCard(c) {
|
|
130
|
-
return `<article class="rk-discover-card rk-card-hover">
|
|
131
|
-
<div class="rk-discover-poster" style="background:${c.grad}">
|
|
132
|
-
<span class="rk-discover-poster-glyph" aria-hidden="true">${c.emoji}</span>
|
|
133
|
-
<span class="rk-discover-dur">${c.duration}</span>
|
|
134
|
-
<div class="rk-discover-scrim"></div>
|
|
135
|
-
<p class="rk-discover-hook">${c.hook}</p>
|
|
136
|
-
</div>
|
|
137
|
-
<div class="rk-discover-body">
|
|
138
|
-
<div class="rk-discover-titlewrap">
|
|
139
|
-
<h3 class="rk-discover-title">${c.title}</h3>
|
|
140
|
-
<p class="rk-discover-metaline">${c.handle} · ${c.forks} forks</p>
|
|
141
|
-
</div>
|
|
142
|
-
<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork">${FORK_ICON} Fork template</button>
|
|
143
|
-
</div>
|
|
144
|
-
</article>`;
|
|
70
|
+
// Functional (non-decorative) SVG glyphs.
|
|
71
|
+
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>`;
|
|
72
|
+
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
|
+
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
|
+
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>`;
|
|
75
|
+
const SOUND_ON_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M18.5 6a9 9 0 0 1 0 12"/></svg>`;
|
|
76
|
+
const SOUND_OFF_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9.5 5 5M21 9.5l-5 5"/></svg>`;
|
|
77
|
+
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
|
+
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
|
+
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>`;
|
|
80
|
+
/** Safe JSON embedding inside a <script> tag (mirrors page-shell.escapeJsonForHtml). */
|
|
81
|
+
function jsonForScript(value) {
|
|
82
|
+
return JSON.stringify(value ?? null)
|
|
83
|
+
.replace(/</g, "\\u003c")
|
|
84
|
+
.replace(/>/g, "\\u003e")
|
|
85
|
+
.replace(/&/g, "\\u0026")
|
|
86
|
+
.replace(/\u2028/g, "\\u2028")
|
|
87
|
+
.replace(/\u2029/g, "\\u2029");
|
|
145
88
|
}
|
|
146
|
-
//
|
|
147
|
-
function
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const style = `z-index:${z};transform:translateY(${ty}px) scale(${scale});opacity:${op};${hidden}`;
|
|
154
|
-
return `<article class="rk-discover-swipe${i === 0 ? " is-front" : ""}" data-swipe style="${style}">
|
|
155
|
-
<div class="rk-discover-swipe-art" style="background:${c.grad}"></div>
|
|
156
|
-
<span class="rk-discover-poster-glyph" aria-hidden="true">${c.emoji}</span>
|
|
157
|
-
<span class="rk-discover-dur">${c.duration}</span>
|
|
158
|
-
<div class="rk-discover-swipe-scrim"></div>
|
|
159
|
-
<div class="rk-discover-swipe-info">
|
|
160
|
-
<p class="rk-discover-hook">${c.hook}</p>
|
|
161
|
-
<div class="rk-discover-swipe-foot">
|
|
162
|
-
<div class="rk-discover-swipe-title">${c.title}</div>
|
|
163
|
-
<div class="rk-discover-swipe-handle">${c.handle} · ${c.forks} forks</div>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</article>`;
|
|
89
|
+
// The segmented Feed / Swipe / Categories toggle (three real links).
|
|
90
|
+
function modeToggle(active) {
|
|
91
|
+
const links = MODES.map((m) => {
|
|
92
|
+
const on = m.mode === active ? " is-active" : "";
|
|
93
|
+
return `<a class="rk-discover-mode${on}" href="/discover${m.mode === "feed" ? "" : "?view=" + m.mode}"${m.mode === active ? ' aria-current="page"' : ""}>${m.label}</a>`;
|
|
94
|
+
}).join("");
|
|
95
|
+
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
167
96
|
}
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
97
|
+
// Server-rendered category skeleton from the real POPULAR_INSPIRATION_GROUPS
|
|
98
|
+
// manifest — always renders (real group labels + real format counts) even
|
|
99
|
+
// before/without the /discover/popular fetch, so categories mode is never blank
|
|
100
|
+
// and degrades gracefully offline. The client enriches each card with real
|
|
101
|
+
// thumbnails + wires an in-page reveal of that group's real templates.
|
|
102
|
+
function categorySkeleton() {
|
|
103
|
+
return POPULAR_INSPIRATION_GROUPS.map((g) => {
|
|
104
|
+
const count = g.items.length;
|
|
105
|
+
const blurb = g.items.slice(0, 4).map((i) => i.title).join(" · ");
|
|
106
|
+
const fallbackQuery = encodeURIComponent(g.label);
|
|
107
|
+
// Placeholder thumb tiles (no media yet) keep the card height stable; the
|
|
108
|
+
// client swaps in real poster media once /discover/popular resolves.
|
|
109
|
+
const thumbs = Array.from({ length: 4 })
|
|
110
|
+
.map(() => `<span class="rk-discover-mini rk-discover-mini-ph"></span>`)
|
|
111
|
+
.join("");
|
|
112
|
+
return `<a class="rk-discover-cat rk-card-hover" href="/discover?q=${fallbackQuery}" data-rk-cat="${rkEscape(g.label)}">
|
|
174
113
|
<div class="rk-discover-cat-head">
|
|
175
114
|
<div class="rk-discover-cat-titlewrap">
|
|
176
|
-
<h3 class="rk-discover-cat-title">${g.label}</h3>
|
|
177
|
-
<p class="rk-discover-cat-count">${
|
|
115
|
+
<h3 class="rk-discover-cat-title">${rkEscape(g.label)}</h3>
|
|
116
|
+
<p class="rk-discover-cat-count" data-rk-cat-count>${count} format${count === 1 ? "" : "s"}</p>
|
|
178
117
|
</div>
|
|
179
|
-
<span class="rk-discover-cat-arrow" aria-hidden="true"
|
|
118
|
+
<span class="rk-discover-cat-arrow" aria-hidden="true">→</span>
|
|
180
119
|
</div>
|
|
181
|
-
<
|
|
182
|
-
<
|
|
120
|
+
<div class="rk-discover-cat-thumbs" data-rk-cat-thumbs>${thumbs}</div>
|
|
121
|
+
<p class="rk-discover-cat-blurb">${rkEscape(blurb)}</p>
|
|
183
122
|
</a>`;
|
|
184
|
-
}
|
|
185
|
-
// ── the segmented Feed / Swipe / Categories toggle (three real links) ───────
|
|
186
|
-
function modeToggle(active) {
|
|
187
|
-
const links = MODES.map((m) => {
|
|
188
|
-
const on = m.mode === active ? " is-active" : "";
|
|
189
|
-
return `<a class="rk-discover-mode${on}" href="/reskin/discover/${m.mode}"${m.mode === active ? ' aria-current="page"' : ""}>${m.label}</a>`;
|
|
190
123
|
}).join("");
|
|
191
|
-
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
192
124
|
}
|
|
193
|
-
|
|
125
|
+
// The fullscreen TikTok-style viewer shell — shared by feed and categories
|
|
126
|
+
// (the client script fills [data-viewer-track] with slides on open).
|
|
127
|
+
function viewerHtml() {
|
|
128
|
+
return `
|
|
129
|
+
<div class="rk-discover-viewer" data-rk-viewer hidden role="dialog" aria-modal="true" aria-label="Template viewer">
|
|
130
|
+
<div class="rk-discover-viewer-scrim" data-viewer-close></div>
|
|
131
|
+
<div class="rk-discover-viewer-track" data-viewer-track></div>
|
|
132
|
+
<div class="rk-discover-viewer-ui">
|
|
133
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-mute aria-label="Toggle sound"><span class="rk-dvm-on">${SOUND_ON_ICON}</span><span class="rk-dvm-off">${SOUND_OFF_ICON}</span></button>
|
|
134
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="rk-discover-viewer-nav">
|
|
137
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-prev aria-label="Previous template">${CHEV_UP_ICON}</button>
|
|
138
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-next aria-label="Next template">${CHEV_DOWN_ICON}</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>`;
|
|
141
|
+
}
|
|
142
|
+
export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
194
143
|
const copy = MODE_COPY[mode];
|
|
195
|
-
|
|
144
|
+
const boot = { ...DEFAULT_INPUT, ...input };
|
|
196
145
|
let modeBody = "";
|
|
146
|
+
// ── FEED ────────────────────────────────────────────────────────────────
|
|
197
147
|
if (mode === "feed") {
|
|
198
|
-
|
|
199
|
-
|
|
148
|
+
modeBody = `
|
|
149
|
+
<div class="rk-discover-feed" data-rk-feed>
|
|
150
|
+
<div class="rk-discover-grid" data-rk-grid></div>
|
|
151
|
+
<div class="rk-discover-state" data-rk-state>Loading templates…</div>
|
|
152
|
+
</div>${viewerHtml()}`;
|
|
200
153
|
}
|
|
201
|
-
//
|
|
154
|
+
// Feed's quick search + bookmarked-only chip share the toolbar row with the
|
|
155
|
+
// mode toggle.
|
|
156
|
+
const toolbarSearch = mode === "feed"
|
|
157
|
+
? `<label class="rk-discover-search">
|
|
158
|
+
<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
|
+
<input type="search" data-rk-search placeholder="Search templates" autocomplete="off" aria-label="Search templates" />
|
|
160
|
+
</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>`
|
|
163
|
+
: "";
|
|
164
|
+
// Categories' in-page group reveal header ("← All categories" + the group
|
|
165
|
+
// title) lives IN the toolbar row, next to the mode toggle (user feedback);
|
|
166
|
+
// the client unhides it while a group is revealed.
|
|
167
|
+
const toolbarReveal = mode === "categories"
|
|
168
|
+
? `<div class="rk-discover-reveal-head" data-rk-reveal-head hidden>
|
|
169
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-cat-back>← All categories</button>
|
|
170
|
+
<h2 class="rk-discover-reveal-title" data-rk-reveal-title></h2>
|
|
171
|
+
</div>`
|
|
172
|
+
: "";
|
|
173
|
+
// ── SWIPE ───────────────────────────────────────────────────────────────
|
|
202
174
|
if (mode === "swipe") {
|
|
203
|
-
const deck = SWIPE_CARDS.map((c, i) => swipeCard(c, i)).join("");
|
|
204
175
|
modeBody = `
|
|
205
|
-
<div class="rk-discover-swipe-wrap">
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<h3>You're all caught up</h3>
|
|
211
|
-
<p class="rk-muted">We're lining up fresh templates that match your brand.</p>
|
|
212
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-restart>↻ Reshuffle the deck</button>
|
|
176
|
+
<div class="rk-discover-swipe-wrap" data-rk-swipe>
|
|
177
|
+
<div class="rk-discover-stage">
|
|
178
|
+
<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
|
+
<div class="rk-discover-deck" data-deck>
|
|
180
|
+
<div class="rk-discover-swipe-state" data-rk-swipe-state>Loading your deck…</div>
|
|
213
181
|
</div>
|
|
182
|
+
<button type="button" class="rk-discover-act rk-discover-act-fork" data-swipe-fork data-swipe-act hidden aria-label="Fork">${CHECK_ICON}</button>
|
|
214
183
|
</div>
|
|
215
|
-
<
|
|
216
|
-
<button type="button" class="rk-discover-act rk-discover-act-skip" data-swipe-skip aria-label="Skip">✕</button>
|
|
217
|
-
<button type="button" class="rk-discover-act rk-discover-act-fork" data-swipe-fork aria-label="Fork">✓</button>
|
|
218
|
-
</div>
|
|
219
|
-
<p class="rk-discover-act-hint" data-actions-hint>Swipe to customize into an ad for your brand.</p>
|
|
184
|
+
<p class="rk-discover-act-hint" data-actions-hint hidden>Swipe to customize into an ad for your brand.</p>
|
|
220
185
|
</div>`;
|
|
221
186
|
}
|
|
222
|
-
// ── CATEGORIES
|
|
187
|
+
// ── CATEGORIES ──────────────────────────────────────────────────────────
|
|
223
188
|
if (mode === "categories") {
|
|
224
|
-
|
|
225
|
-
|
|
189
|
+
modeBody = `
|
|
190
|
+
<div class="rk-discover-catwrap" data-rk-cats>
|
|
191
|
+
<div class="rk-discover-cats" data-rk-cats-grid>${categorySkeleton()}</div>
|
|
192
|
+
<section class="rk-discover-cat-reveal" data-rk-cat-reveal hidden>
|
|
193
|
+
<div class="rk-discover-grid" data-rk-reveal-grid></div>
|
|
194
|
+
</section>
|
|
195
|
+
<div class="rk-discover-state rk-discover-cats-state" data-rk-cats-state hidden></div>
|
|
196
|
+
</div>${viewerHtml()}`;
|
|
226
197
|
}
|
|
227
198
|
const body = `
|
|
228
199
|
<main class="rk-container rk-page">
|
|
229
200
|
<div class="rk-page-head rk-discover-head">
|
|
230
|
-
<span class="rk-badge">${copy.badge}</span>
|
|
231
201
|
<h1 class="rk-h1">Discover</h1>
|
|
232
202
|
<p class="rk-sub">${copy.sub}</p>
|
|
233
203
|
</div>
|
|
234
|
-
|
|
204
|
+
<div class="rk-discover-toolbar">
|
|
205
|
+
${modeToggle(mode)}
|
|
206
|
+
${toolbarReveal}
|
|
207
|
+
${toolbarSearch}
|
|
208
|
+
</div>
|
|
235
209
|
${modeBody}
|
|
210
|
+
<script type="application/json" data-rk-discover-boot>${jsonForScript(boot)}</script>
|
|
236
211
|
</main>`;
|
|
237
212
|
const pageCss = `
|
|
238
213
|
.rk-discover-head{margin-bottom:22px}
|
|
239
214
|
|
|
215
|
+
/* toolbar row: mode toggle + (feed) quick search share one line, wrap on mobile */
|
|
216
|
+
.rk-discover-toolbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:26px}
|
|
217
|
+
|
|
240
218
|
/* segmented Feed / Swipe / Categories toggle — three real links, gold active */
|
|
241
|
-
.rk-discover-modes{display:inline-flex;gap:4px;background:var(--rk-n-100);border:1px solid var(--rk-border);
|
|
242
|
-
border-radius:var(--rk-r-full);padding:4px;box-shadow:var(--rk-shadow-xs);
|
|
219
|
+
.rk-discover-modes{display:inline-flex;flex:none;gap:4px;background:var(--rk-n-100);border:1px solid var(--rk-border);
|
|
220
|
+
border-radius:var(--rk-r-full);padding:4px;box-shadow:var(--rk-shadow-xs);max-width:100%;overflow:auto;scrollbar-width:none}
|
|
243
221
|
.rk-discover-modes::-webkit-scrollbar{display:none}
|
|
244
222
|
.rk-discover-mode{display:inline-flex;align-items:center;gap:7px;padding:9px 18px;border-radius:var(--rk-r-full);
|
|
245
223
|
font-size:14px;font-weight:600;color:var(--rk-n-600);white-space:nowrap;
|
|
@@ -247,62 +225,176 @@ export function renderReskinDiscover(mode = "feed") {
|
|
|
247
225
|
.rk-discover-mode:hover{color:var(--rk-ink)}
|
|
248
226
|
.rk-discover-mode.is-active{background:var(--rk-gold-500);color:var(--rk-ink);font-weight:700;box-shadow:var(--rk-shadow-sm)}
|
|
249
227
|
|
|
250
|
-
/* ── feed: poster grid ── */
|
|
228
|
+
/* ── feed: quick search (lives in the toolbar row) + poster grid ── */
|
|
229
|
+
.rk-discover-search{flex:1;min-width:220px;display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--rk-border-strong);
|
|
230
|
+
border-radius:var(--rk-r-full);padding:10px 16px;box-shadow:var(--rk-shadow-xs);
|
|
231
|
+
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
232
|
+
.rk-discover-search:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
233
|
+
.rk-discover-search-ic{color:var(--rk-text-faint);display:grid;place-items:center;flex:none}
|
|
234
|
+
.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
|
+
.rk-discover-search input::placeholder{color:var(--rk-n-400)}
|
|
236
|
+
.rk-discover-count{white-space:nowrap}
|
|
237
|
+
|
|
251
238
|
.rk-discover-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(212px,1fr));gap:22px}
|
|
252
239
|
@media(max-width:520px){.rk-discover-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}}
|
|
253
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}
|
|
254
|
-
.rk-discover-poster{position:relative;aspect-ratio:9/16;
|
|
255
|
-
.rk-discover-poster-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.rk-discover-
|
|
261
|
-
|
|
241
|
+
.rk-discover-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:var(--rk-n-100);color:inherit}
|
|
242
|
+
.rk-discover-poster img,.rk-discover-poster video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
243
|
+
.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);
|
|
244
|
+
background:repeating-linear-gradient(135deg,var(--rk-n-100),var(--rk-n-100) 12px,var(--rk-n-50) 12px,var(--rk-n-50) 24px)}
|
|
245
|
+
.rk-discover-dur{position:absolute;top:12px;right:12px;z-index:3;font-family:var(--rk-font-body);font-size:11.5px;font-weight:700;color:#fff;
|
|
246
|
+
background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);padding:4px 9px;border-radius:var(--rk-r-full)}
|
|
247
|
+
.rk-discover-scrim{position:absolute;inset:0;z-index:1;background:linear-gradient(to top,rgba(0,0,0,.28) 0%,transparent 42%)}
|
|
248
|
+
/* wrap = positioning context so the bookmark button can sit NEXT TO (not
|
|
249
|
+
inside) the poster anchor — a button inside an <a> is invalid HTML */
|
|
250
|
+
.rk-discover-posterwrap{position:relative}
|
|
251
|
+
.rk-discover-bm{position:absolute;right:10px;bottom:10px;z-index:4;width:34px;height:34px;border:0;border-radius:var(--rk-r-full);
|
|
252
|
+
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
|
+
transition:transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
254
|
+
.rk-discover-posterwrap:hover .rk-discover-bm{display:grid}
|
|
255
|
+
.rk-discover-bm:hover{transform:scale(1.08);background:rgba(0,0,0,.6)}
|
|
256
|
+
.rk-discover-bm.is-on{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
257
|
+
.rk-discover-bm.is-on svg{fill:currentColor}
|
|
262
258
|
.rk-discover-body{padding:15px 16px 16px;display:grid;gap:13px}
|
|
263
|
-
.rk-discover-titlewrap{display:grid;gap:
|
|
264
|
-
.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
|
|
265
|
-
|
|
259
|
+
.rk-discover-titlewrap{display:grid;gap:6px}
|
|
260
|
+
.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
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
262
|
+
.rk-discover-desc{font-size:12.5px;font-weight:500;color:var(--rk-text-muted);line-height:1.4;
|
|
263
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
266
264
|
.rk-discover-fork{gap:7px}
|
|
267
265
|
.rk-discover-fork svg{margin-top:-1px}
|
|
266
|
+
.rk-discover-fork-disabled{opacity:.6;cursor:default;pointer-events:none}
|
|
267
|
+
.rk-discover-badges{display:flex;flex-wrap:wrap;gap:6px}
|
|
268
|
+
|
|
269
|
+
/* hover-to-unmute speaker badge (video posters only): hidden until hover,
|
|
270
|
+
flips to a gold "sound on" chip once the browser actually allows audio */
|
|
271
|
+
.rk-discover-sound{position:absolute;left:12px;bottom:12px;z-index:3;width:32px;height:32px;display:none;place-items:center;color:#fff;
|
|
272
|
+
background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border-radius:var(--rk-r-full);pointer-events:none}
|
|
273
|
+
.rk-discover-sound span{display:grid;place-items:center;line-height:0}
|
|
274
|
+
.rk-discover-posterwrap:hover .rk-discover-sound{display:grid}
|
|
275
|
+
.rk-discover-sound .rk-ds-on{display:none}
|
|
276
|
+
.rk-discover-posterwrap.is-sound .rk-discover-sound{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
277
|
+
.rk-discover-posterwrap.is-sound .rk-ds-on{display:grid}
|
|
278
|
+
.rk-discover-posterwrap.is-sound .rk-ds-off{display:none}
|
|
279
|
+
|
|
280
|
+
/* bookmarked-only toolbar chip (feed) — count span collapses when empty */
|
|
281
|
+
.rk-discover-bmfilter{flex:none;display:inline-flex;align-items:center;gap:7px;padding:10px 15px;border-radius:var(--rk-r-full);
|
|
282
|
+
border:1px solid var(--rk-border-strong);background:#fff;color:var(--rk-n-600);font-family:var(--rk-font-body);font-size:13px;font-weight:700;
|
|
283
|
+
cursor:pointer;box-shadow:var(--rk-shadow-xs);
|
|
284
|
+
transition:color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
285
|
+
.rk-discover-bmfilter:hover{color:var(--rk-ink);border-color:var(--rk-gold-600)}
|
|
286
|
+
.rk-discover-bmfilter.is-active{background:var(--rk-gold-500);border-color:transparent;color:var(--rk-ink)}
|
|
287
|
+
.rk-discover-bmfilter.is-active svg{fill:currentColor}
|
|
288
|
+
.rk-discover-bmfilter-count:empty{display:none}
|
|
289
|
+
|
|
290
|
+
/* load / empty / error state (feed + categories) */
|
|
291
|
+
.rk-discover-state{padding:48px 16px;text-align:center;color:var(--rk-text-muted);font-size:14px;
|
|
292
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs)}
|
|
293
|
+
.rk-discover-state[hidden]{display:none}
|
|
294
|
+
.rk-discover-state.is-error{color:#b91c1c;background:var(--rk-red-tint);border-color:#fecaca;font-weight:500}
|
|
295
|
+
.rk-discover-state .rk-btn{margin-top:14px}
|
|
296
|
+
|
|
297
|
+
/* ── feed: TikTok-style fullscreen viewer (click a card → vertical deck) ── */
|
|
298
|
+
html.rk-noscroll,html.rk-noscroll body{overflow:hidden}
|
|
299
|
+
.rk-discover-viewer{position:fixed;inset:0;z-index:120}
|
|
300
|
+
.rk-discover-viewer[hidden]{display:none}
|
|
301
|
+
.rk-discover-viewer-scrim{position:absolute;inset:0;background:rgba(15,12,5,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
|
302
|
+
.rk-discover-viewer-track{position:absolute;inset:0;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none}
|
|
303
|
+
.rk-discover-viewer-track::-webkit-scrollbar{display:none}
|
|
304
|
+
.rk-discover-viewer-slide{height:100%;display:grid;place-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:20px 0}
|
|
305
|
+
.rk-discover-viewer-stage{position:relative;width:min(92vw,calc(93vh * 9 / 16));aspect-ratio:9/16;max-height:100%;margin:0;
|
|
306
|
+
border-radius:var(--rk-r-3xl);overflow:hidden;background:#000;box-shadow:var(--rk-shadow-xl)}
|
|
307
|
+
.rk-discover-viewer-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
|
|
308
|
+
video.rk-discover-viewer-media{cursor:pointer}
|
|
309
|
+
.rk-discover-viewer-pause{position:absolute;inset:0;z-index:2;display:none;place-items:center;color:rgba(255,255,255,.92);pointer-events:none;
|
|
310
|
+
filter:drop-shadow(0 4px 14px rgba(0,0,0,.55))}
|
|
311
|
+
.rk-discover-viewer-slide.is-paused .rk-discover-viewer-pause{display:grid}
|
|
312
|
+
.rk-discover-viewer-shade{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:1;pointer-events:none;
|
|
313
|
+
background:linear-gradient(to top,rgba(0,0,0,.74) 0%,rgba(0,0,0,.22) 62%,transparent 100%)}
|
|
314
|
+
/* right padding reserves the bookmark rail so text never runs under it */
|
|
315
|
+
.rk-discover-viewer-info{position:absolute;left:0;right:0;bottom:0;z-index:3;padding:20px 78px 20px 20px;display:grid;gap:7px;justify-items:start;pointer-events:none}
|
|
316
|
+
.rk-discover-viewer-bm{position:absolute;right:14px;bottom:22px;z-index:4;width:48px;height:48px;border-radius:var(--rk-r-full);
|
|
317
|
+
border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.14);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
|
|
318
|
+
color:#fff;display:grid;place-items:center;cursor:pointer;
|
|
319
|
+
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
320
|
+
.rk-discover-viewer-bm:hover{background:rgba(255,255,255,.26);transform:scale(1.06)}
|
|
321
|
+
.rk-discover-viewer-bm.is-on{background:var(--rk-gold-500);border-color:transparent;color:var(--rk-ink)}
|
|
322
|
+
.rk-discover-viewer-bm.is-on svg{fill:currentColor}
|
|
323
|
+
.rk-discover-viewer-meta{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:rgba(255,255,255,.72)}
|
|
324
|
+
.rk-discover-viewer-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.12rem;line-height:1.2;letter-spacing:-.01em;color:#fff}
|
|
325
|
+
.rk-discover-viewer-desc{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.82);line-height:1.4;
|
|
326
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
327
|
+
.rk-discover-viewer-fork{pointer-events:auto;gap:7px;margin-top:5px}
|
|
328
|
+
.rk-discover-viewer-ui{position:absolute;top:max(16px,env(safe-area-inset-top,0px));right:16px;z-index:5;display:flex;gap:10px}
|
|
329
|
+
.rk-discover-viewer-btn{width:44px;height:44px;border-radius:var(--rk-r-full);border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.12);
|
|
330
|
+
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;display:grid;place-items:center;cursor:pointer;
|
|
331
|
+
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
332
|
+
.rk-discover-viewer-btn:hover{background:rgba(255,255,255,.24);transform:scale(1.06)}
|
|
333
|
+
.rk-discover-viewer-btn span{display:grid;place-items:center;line-height:0}
|
|
334
|
+
.rk-discover-viewer .rk-dvm-off{display:none}
|
|
335
|
+
.rk-discover-viewer.is-muted .rk-dvm-on{display:none}
|
|
336
|
+
.rk-discover-viewer.is-muted .rk-dvm-off{display:grid}
|
|
337
|
+
.rk-discover-viewer-nav{position:absolute;right:18px;top:50%;transform:translateY(-50%);z-index:5;display:grid;gap:12px}
|
|
338
|
+
@media(hover:none){.rk-discover-viewer-nav{display:none}}
|
|
339
|
+
@media(max-width:640px){
|
|
340
|
+
.rk-discover-viewer-nav{display:none}
|
|
341
|
+
.rk-discover-viewer-slide{padding:0}
|
|
342
|
+
.rk-discover-viewer-stage{width:100%;height:100%;max-height:none;aspect-ratio:auto;border-radius:0;box-shadow:none}
|
|
343
|
+
}
|
|
268
344
|
|
|
269
345
|
/* ── swipe: single-card deck ── */
|
|
270
346
|
.rk-discover-swipe-wrap{display:grid;justify-items:center;gap:18px;padding:4px 0 8px}
|
|
271
|
-
.rk-discover-
|
|
272
|
-
.rk-discover-
|
|
273
|
-
|
|
274
|
-
.rk-discover-swipe
|
|
347
|
+
.rk-discover-deck{position:relative;width:min(322px,84vw);aspect-ratio:9/16;margin:6px 0}
|
|
348
|
+
.rk-discover-swipe-state{position:absolute;inset:0;z-index:1;display:grid;place-content:center;justify-items:center;gap:10px;text-align:center;padding:28px;
|
|
349
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);color:var(--rk-text-muted);font-size:14px}
|
|
350
|
+
.rk-discover-swipe-state[hidden]{display:none}
|
|
351
|
+
.rk-discover-swipe-state.is-error{color:#b91c1c}
|
|
352
|
+
.rk-discover-swipe-state-title{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:700;color:var(--rk-ink)}
|
|
353
|
+
.rk-discover-swipe-state .rk-btn{margin-top:6px}
|
|
354
|
+
.rk-discover-spinner{width:30px;height:30px;border-radius:50%;border:3px solid var(--rk-gold-tint2);border-top-color:var(--rk-gold-700);animation:rk-discover-spin .8s linear infinite}
|
|
355
|
+
@keyframes rk-discover-spin{to{transform:rotate(360deg)}}
|
|
356
|
+
.rk-discover-swipe{position:absolute;inset:0;border-radius:var(--rk-r-3xl);overflow:hidden;border:1px solid var(--rk-border);background:#0b0b0b;
|
|
275
357
|
box-shadow:var(--rk-shadow-xl);transform-origin:center bottom;will-change:transform;
|
|
276
358
|
transition:transform .38s var(--rk-ease),opacity .38s var(--rk-ease)}
|
|
277
|
-
.rk-discover-swipe-
|
|
278
|
-
.rk-discover-swipe
|
|
279
|
-
|
|
280
|
-
.rk-discover-swipe-
|
|
281
|
-
.rk-discover-swipe
|
|
282
|
-
.rk-discover-swipe-
|
|
283
|
-
.rk-discover-swipe-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
.
|
|
287
|
-
|
|
288
|
-
.rk-discover-
|
|
289
|
-
.rk-discover-
|
|
290
|
-
.rk-discover-empty .rk-btn{margin-top:8px}
|
|
291
|
-
|
|
292
|
-
.rk-discover-actions{display:flex;align-items:center;justify-content:center;gap:20px}
|
|
359
|
+
.rk-discover-swipe-frame{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;pointer-events:none;background:#000}
|
|
360
|
+
.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}
|
|
362
|
+
.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
|
+
.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
|
+
.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
|
+
.rk-discover-swipe-caption{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.84);line-height:1.35;
|
|
366
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
367
|
+
|
|
368
|
+
/* deck flanked by skip/fork — buttons ride the card's vertical center, 1fr gutters absorb spare width.
|
|
369
|
+
Explicit column placement keeps the deck centered when the buttons are display:none (empty/caught-up states). */
|
|
370
|
+
.rk-discover-stage{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:20px;width:100%}
|
|
371
|
+
.rk-discover-deck{grid-column:2}
|
|
293
372
|
.rk-discover-act{width:60px;height:60px;flex:none;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);background:#fff;
|
|
294
|
-
display:grid;place-items:center;
|
|
295
|
-
transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
373
|
+
display:grid;place-items:center;cursor:pointer;box-shadow:var(--rk-shadow-md);
|
|
374
|
+
transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
375
|
+
.rk-discover-act[hidden]{display:none}
|
|
376
|
+
.rk-discover-act-skip{grid-column:1;justify-self:end}
|
|
377
|
+
.rk-discover-act-fork{grid-column:3;justify-self:start}
|
|
378
|
+
/* no side room on small phones — card spans, buttons drop under it as a pair */
|
|
379
|
+
@media(max-width:560px){
|
|
380
|
+
.rk-discover-stage{grid-template-columns:auto auto;grid-template-areas:"deck deck" "skip fork";justify-content:center;column-gap:26px;row-gap:16px}
|
|
381
|
+
.rk-discover-deck{grid-area:deck}
|
|
382
|
+
.rk-discover-act-skip{grid-area:skip;justify-self:center}
|
|
383
|
+
.rk-discover-act-fork{grid-area:fork;justify-self:center}
|
|
384
|
+
}
|
|
296
385
|
.rk-discover-act:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
|
|
386
|
+
.rk-discover-act:disabled{opacity:.5;cursor:default;transform:none}
|
|
297
387
|
.rk-discover-act-skip{color:var(--rk-n-500)}
|
|
298
388
|
.rk-discover-act-skip:hover{color:var(--rk-red);border-color:var(--rk-red-tint)}
|
|
299
389
|
.rk-discover-act-fork{background:var(--rk-gold-500);color:var(--rk-ink);border-color:transparent}
|
|
300
390
|
.rk-discover-act-fork:hover{background:var(--rk-gold-700);box-shadow:0 8px 22px -6px rgba(252,185,0,.6)}
|
|
301
391
|
.rk-discover-act-hint{font-size:12.5px;font-weight:600;color:var(--rk-text-faint);text-align:center;letter-spacing:.01em}
|
|
392
|
+
.rk-discover-act-hint[hidden]{display:none}
|
|
302
393
|
|
|
303
394
|
/* ── categories: browse-by-format cards ── */
|
|
304
395
|
.rk-discover-cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:20px}
|
|
305
396
|
@media(max-width:560px){.rk-discover-cats{grid-template-columns:1fr}}
|
|
397
|
+
.rk-discover-cats[hidden]{display:none}
|
|
306
398
|
.rk-discover-cat{display:flex;flex-direction:column;gap:14px;background:var(--rk-surface);border:1px solid var(--rk-border);
|
|
307
399
|
border-radius:var(--rk-r-3xl);padding:22px;box-shadow:var(--rk-shadow-card);color:inherit}
|
|
308
400
|
.rk-discover-cat-head{display:flex;align-items:center;gap:14px}
|
|
@@ -311,66 +403,690 @@ export function renderReskinDiscover(mode = "feed") {
|
|
|
311
403
|
.rk-discover-cat-count{font-size:12.5px;font-weight:600;color:var(--rk-text-muted)}
|
|
312
404
|
.rk-discover-cat-arrow{flex:none;font-size:18px;color:var(--rk-n-400);transition:transform var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
313
405
|
.rk-discover-cat:hover .rk-discover-cat-arrow{transform:translateX(3px);color:var(--rk-gold-700)}
|
|
314
|
-
.rk-discover-cat-blurb{font-size:13px;color:var(--rk-text-muted);line-height:1.45
|
|
406
|
+
.rk-discover-cat-blurb{font-size:13px;color:var(--rk-text-muted);line-height:1.45;
|
|
407
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
315
408
|
.rk-discover-cat-thumbs{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:8px}
|
|
316
|
-
.rk-discover-mini{aspect-ratio:9/16;border-radius:var(--rk-r-lg);
|
|
409
|
+
.rk-discover-mini{position:relative;aspect-ratio:9/16;border-radius:var(--rk-r-lg);overflow:hidden;background:var(--rk-n-100);box-shadow:var(--rk-shadow-xs)}
|
|
410
|
+
.rk-discover-mini img,.rk-discover-mini video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
411
|
+
.rk-discover-mini-ph{background:linear-gradient(135deg,var(--rk-n-100),var(--rk-n-200))}
|
|
412
|
+
|
|
413
|
+
.rk-discover-cat-reveal[hidden]{display:none}
|
|
414
|
+
/* reveal header shares the toolbar row with the mode toggle (user feedback) */
|
|
415
|
+
.rk-discover-reveal-head{display:flex;align-items:center;gap:12px;min-width:0}
|
|
416
|
+
.rk-discover-reveal-head[hidden]{display:none}
|
|
417
|
+
.rk-discover-reveal-title{font-family:var(--rk-font-display);font-size:var(--rk-text-xl);font-weight:800;color:var(--rk-ink);letter-spacing:-.02em;
|
|
418
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
419
|
+
.rk-discover-cats-state[hidden]{display:none}
|
|
317
420
|
`;
|
|
318
421
|
const script = `
|
|
319
422
|
(function(){
|
|
320
423
|
var root=document;
|
|
321
|
-
|
|
322
|
-
var
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
424
|
+
var bootEl=root.querySelector('[data-rk-discover-boot]');
|
|
425
|
+
var boot={};
|
|
426
|
+
try{ boot=JSON.parse(bootEl&&bootEl.textContent?bootEl.textContent:'{}')||{}; }catch(e){ boot={}; }
|
|
427
|
+
var feedEndpoint=boot.feedEndpoint||'/discover/feed';
|
|
428
|
+
var popularEndpoint=boot.popularEndpoint||'/discover/popular';
|
|
429
|
+
var swipeStackEndpoint=boot.swipeStackEndpoint||'/discover/swipe/stack';
|
|
430
|
+
var swipeRefillEndpoint=boot.swipeRefillEndpoint||'/discover/swipe/refill';
|
|
431
|
+
var swipeBase=boot.swipeBase||'/discover/swipe';
|
|
432
|
+
var editorBase=boot.editorBase||'/editor';
|
|
433
|
+
var accountId=boot.accountId||'';
|
|
434
|
+
|
|
435
|
+
function esc(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
436
|
+
function attr(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
|
437
|
+
function isVideo(u){ return Boolean(u)&&/\\.(mp4|webm|ogg|mov)(\\?|#|$)/i.test(u); }
|
|
438
|
+
// previewUrl falls back to the template's raw videoUrl, which is often a
|
|
439
|
+
// signed/extension-less URL — so treat "not a known image" as video, never
|
|
440
|
+
// the other way round, or those templates render a broken <img> (no preview).
|
|
441
|
+
function isImage(u){ return Boolean(u)&&/\\.(png|jpe?g|gif|webp|avif|svg)(\\?|#|$)/i.test(u); }
|
|
442
|
+
function withAccount(u){
|
|
443
|
+
try{ var url=new URL(u, window.location.href); if(accountId) url.searchParams.set('account', accountId); return url.toString(); }
|
|
444
|
+
catch(e){ return u; }
|
|
445
|
+
}
|
|
446
|
+
function editorUrl(id){
|
|
447
|
+
var u=editorBase+'/'+encodeURIComponent(String(id));
|
|
448
|
+
return accountId? u+'?account='+encodeURIComponent(accountId) : u;
|
|
449
|
+
}
|
|
450
|
+
function fmtDuration(s){
|
|
451
|
+
s=Number(s); if(!s||!isFinite(s)||s<=0) return '';
|
|
452
|
+
if(s>=60){ var m=Math.floor(s/60); var r=Math.round(s%60); return m+':'+(r<10?'0':'')+r; }
|
|
453
|
+
return (s>=10? s.toFixed(0) : s.toFixed(1))+'s';
|
|
454
|
+
}
|
|
455
|
+
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
|
+
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
|
+
var EDIT_ICON=${JSON.stringify(EDIT_ICON)};
|
|
458
|
+
var BOOKMARK_ICON=${JSON.stringify(BOOKMARK_ICON)};
|
|
459
|
+
var SOUND_ON_ICON=${JSON.stringify(SOUND_ON_ICON)};
|
|
460
|
+
var SOUND_OFF_ICON=${JSON.stringify(SOUND_OFF_ICON)};
|
|
461
|
+
var PLAY_ICON=${JSON.stringify(PLAY_ICON)};
|
|
462
|
+
|
|
463
|
+
// ── bookmarks: client-side saves in localStorage (no backend favorite
|
|
464
|
+
// endpoint exists), shared by feed cards, category reveal cards and the
|
|
465
|
+
// viewer slides. One delegated document listener toggles any [data-bm].
|
|
466
|
+
var BM_KEY='rk-discover-bookmarks';
|
|
467
|
+
var bookmarks=(function(){
|
|
468
|
+
try{ var v=JSON.parse(window.localStorage.getItem(BM_KEY)||'[]'); return new Set(Array.isArray(v)?v.map(String):[]); }
|
|
469
|
+
catch(e){ return new Set(); }
|
|
470
|
+
})();
|
|
471
|
+
var bmChangeHandlers=[];
|
|
472
|
+
function isBookmarked(id){ return Boolean(id)&&bookmarks.has(String(id)); }
|
|
473
|
+
function syncBmButtons(){
|
|
474
|
+
root.querySelectorAll('[data-bm]').forEach(function(b){
|
|
475
|
+
var on=isBookmarked(b.getAttribute('data-bm'));
|
|
476
|
+
b.classList.toggle('is-on',on);
|
|
477
|
+
b.setAttribute('aria-pressed',on?'true':'false');
|
|
478
|
+
b.setAttribute('aria-label',on?'Remove bookmark':'Bookmark');
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function toggleBookmark(id){
|
|
482
|
+
id=String(id||''); if(!id) return;
|
|
483
|
+
if(bookmarks.has(id)) bookmarks.delete(id); else bookmarks.add(id);
|
|
484
|
+
try{ window.localStorage.setItem(BM_KEY, JSON.stringify(Array.from(bookmarks))); }catch(e){}
|
|
485
|
+
syncBmButtons();
|
|
486
|
+
bmChangeHandlers.forEach(function(fn){ try{ fn(id); }catch(e){} });
|
|
487
|
+
}
|
|
488
|
+
function bmBtnHtml(id, cls){
|
|
489
|
+
if(!id) return '';
|
|
490
|
+
var on=isBookmarked(id);
|
|
491
|
+
return '<button type="button" class="'+cls+(on?' is-on':'')+'" data-bm="'+attr(id)+'" aria-pressed="'+(on?'true':'false')+'" aria-label="'+(on?'Remove bookmark':'Bookmark')+'">'+BOOKMARK_ICON+'</button>';
|
|
492
|
+
}
|
|
493
|
+
document.addEventListener('click',function(e){
|
|
494
|
+
var b=(e.target&&e.target.closest)?e.target.closest('[data-bm]'):null;
|
|
495
|
+
if(!b) return;
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
toggleBookmark(b.getAttribute('data-bm'));
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ── shared: pause/resume a grid's muted autoplay (while the viewer is open) ──
|
|
501
|
+
function parkVideos(container){
|
|
502
|
+
if(!container) return;
|
|
503
|
+
container.querySelectorAll('video').forEach(function(o){ o.muted=true; o.pause(); });
|
|
504
|
+
container.querySelectorAll('.rk-discover-posterwrap.is-sound').forEach(function(p){ p.classList.remove('is-sound'); });
|
|
505
|
+
}
|
|
506
|
+
function resumeVideos(container){
|
|
507
|
+
if(!container) return;
|
|
508
|
+
container.querySelectorAll('video').forEach(function(o){ o.muted=true; var pr=o.play(); if(pr&&pr.catch) pr.catch(function(){}); });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function friendlyError(err){
|
|
512
|
+
var msg=(err&&err.message)?String(err.message):'';
|
|
513
|
+
if(!msg||/failed to fetch|networkerror|load failed/i.test(msg)) return "Couldn't reach the server — check your connection, or sign in to keep browsing.";
|
|
514
|
+
return msg;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function mediaHtml(t){
|
|
518
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
519
|
+
if(!url) return '<div class="rk-discover-media-empty">'+(t.status==='processing'?'Downloading…':'No preview')+'</div>';
|
|
520
|
+
if(isImage(url)) return '<img src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'" loading="lazy" decoding="async">';
|
|
521
|
+
// #t=0.1 paints the first frame even where autoplay is blocked.
|
|
522
|
+
return '<video src="'+attr(url+'#t=0.1')+'" muted loop autoplay playsinline preload="metadata" tabindex="-1"></video>';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Real feed/category poster card. The "Open Editor" CTA always deep-links
|
|
526
|
+
// into the real editor; the poster itself carries data-rk-view=<index> so a
|
|
527
|
+
// plain click opens the TikTok-style viewer instead (cmd/middle-click still
|
|
528
|
+
// opens the editor via the untouched href). No platform/source pill on the
|
|
529
|
+
// poster (user feedback) — source shows only in the viewer's meta line.
|
|
530
|
+
function posterCardHtml(t, viewIdx){
|
|
531
|
+
var id=t.templateId||t.template_id||'';
|
|
532
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
533
|
+
var hasVideo=Boolean(url)&&!isImage(url);
|
|
534
|
+
var pending=(t.status==='processing'||t.status==='failed');
|
|
535
|
+
var viewable=!pending&&Boolean(url)&&typeof viewIdx==='number'&&viewIdx>=0;
|
|
536
|
+
var dur=fmtDuration(t.durationSeconds);
|
|
537
|
+
var desc=String(t.viralDna||t.summary||'').trim();
|
|
538
|
+
var posterInner=mediaHtml(t)
|
|
539
|
+
+(dur?'<span class="rk-discover-dur">'+esc(dur)+'</span>':'')
|
|
540
|
+
+'<div class="rk-discover-scrim"></div>'
|
|
541
|
+
+(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
|
+
var poster=pending
|
|
543
|
+
? '<div class="rk-discover-poster">'+posterInner+'</div>'
|
|
544
|
+
: '<a class="rk-discover-poster" href="'+attr(editorUrl(id))+'"'+(viewable?' data-rk-view="'+viewIdx+'"':'')+' aria-label="'+attr((viewable?'Preview ':'Open ')+(t.title||id)+(viewable?'':' in the editor'))+'">'+posterInner+'</a>';
|
|
545
|
+
// bookmark sits NEXT TO the anchor inside the wrap (button-in-anchor is
|
|
546
|
+
// invalid HTML); the wrap is the shared hover context for sound + bookmark
|
|
547
|
+
poster='<div class="rk-discover-posterwrap">'+poster+(pending?'':bmBtnHtml(id,'rk-discover-bm'))+'</div>';
|
|
548
|
+
var cta=pending
|
|
549
|
+
? '<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
|
+
: '<a class="rk-btn rk-btn-gold rk-btn-sm rk-btn-block rk-discover-fork" href="'+attr(editorUrl(id))+'">'+EDIT_ICON+' Open Editor</a>';
|
|
551
|
+
return '<article class="rk-discover-card rk-card-hover">'
|
|
552
|
+
+ poster
|
|
553
|
+
+ '<div class="rk-discover-body">'
|
|
554
|
+
+ '<div class="rk-discover-titlewrap">'
|
|
555
|
+
+ '<h3 class="rk-discover-title">'+esc(t.title||id)+'</h3>'
|
|
556
|
+
+ (desc?'<p class="rk-discover-desc">'+esc(desc)+'</p>':'')
|
|
557
|
+
+ '</div>'
|
|
558
|
+
+ cta
|
|
559
|
+
+ '</div>'
|
|
560
|
+
+ '</article>';
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── hover audio: unmute a poster's (muted, autoplaying) video while the
|
|
564
|
+
// pointer is over it; only one card is ever audible at a time. Browsers may
|
|
565
|
+
// refuse unmuted playback before the user's first click (autoplay policy) —
|
|
566
|
+
// in that case we verify shortly after and fall back to muted playback
|
|
567
|
+
// instead of leaving a frozen poster. Delegated, so grid re-renders keep it.
|
|
568
|
+
function bindHoverAudio(container){
|
|
569
|
+
if(!container) return;
|
|
570
|
+
function posterOf(e){ var t=e.target; return (t&&t.closest)?t.closest('.rk-discover-posterwrap'):null; }
|
|
571
|
+
function setSound(poster,on){
|
|
572
|
+
var v=poster.querySelector('video'); if(!v) return;
|
|
573
|
+
if(on){
|
|
574
|
+
container.querySelectorAll('video').forEach(function(o){ if(o!==v) o.muted=true; });
|
|
575
|
+
container.querySelectorAll('.rk-discover-posterwrap.is-sound').forEach(function(p){ if(p!==poster) p.classList.remove('is-sound'); });
|
|
576
|
+
v.muted=false;
|
|
577
|
+
if(v.paused){ var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){}); }
|
|
578
|
+
window.setTimeout(function(){
|
|
579
|
+
if(v.muted) return; // already reverted (pointer left)
|
|
580
|
+
if(v.paused){ v.muted=true; poster.classList.remove('is-sound'); var pr2=v.play(); if(pr2&&pr2.catch) pr2.catch(function(){}); }
|
|
581
|
+
else poster.classList.add('is-sound');
|
|
582
|
+
},70);
|
|
583
|
+
} else {
|
|
584
|
+
v.muted=true; poster.classList.remove('is-sound');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
container.addEventListener('mouseover',function(e){
|
|
588
|
+
var p=posterOf(e); if(!p) return;
|
|
589
|
+
if(e.relatedTarget&&p.contains(e.relatedTarget)) return; // still inside the same poster
|
|
590
|
+
setSound(p,true);
|
|
591
|
+
});
|
|
592
|
+
container.addEventListener('mouseout',function(e){
|
|
593
|
+
var p=posterOf(e); if(!p) return;
|
|
594
|
+
if(e.relatedTarget&&p.contains(e.relatedTarget)) return;
|
|
595
|
+
setSound(p,false);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── TikTok-style viewer modal — SHARED by the feed grid and the categories
|
|
600
|
+
// reveal grid. open(list, clickedIdx, park): list is the template array
|
|
601
|
+
// behind the grid the user clicked, clickedIdx the clicked card index in
|
|
602
|
+
// that list, park the grid container whose muted autoplay is paused while
|
|
603
|
+
// the modal owns audio + CPU. Slides hydrate lazily (active +/-1); the click
|
|
604
|
+
// that opens the modal is a real user gesture, so the active video plays
|
|
605
|
+
// WITH sound (with a muted-playback fallback if the browser still refuses).
|
|
606
|
+
var viewerCtl=(function(){
|
|
607
|
+
var viewer=root.querySelector('[data-rk-viewer]');
|
|
608
|
+
if(!viewer) return null;
|
|
609
|
+
var vTrack=viewer.querySelector('[data-viewer-track]');
|
|
610
|
+
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null; var parkEl=null;
|
|
611
|
+
|
|
612
|
+
function viewerSlideHtml(t,i){
|
|
613
|
+
var id=t.templateId||t.template_id||'';
|
|
614
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
615
|
+
var media=isImage(url)
|
|
616
|
+
? '<img class="rk-discover-viewer-media" src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'">'
|
|
617
|
+
: '<video class="rk-discover-viewer-media" data-vsrc="'+attr(url)+'" loop playsinline preload="none"></video>';
|
|
618
|
+
var meta=[];
|
|
619
|
+
var srcL=sourceLabel(t.sourceType); if(srcL) meta.push(esc(srcL));
|
|
620
|
+
var dur=fmtDuration(t.durationSeconds); if(dur) meta.push(esc(dur));
|
|
621
|
+
var desc=String(t.viralDna||t.summary||'').trim();
|
|
622
|
+
return '<section class="rk-discover-viewer-slide" data-vslide="'+i+'">'
|
|
623
|
+
+'<figure class="rk-discover-viewer-stage">'
|
|
624
|
+
+ media
|
|
625
|
+
+'<span class="rk-discover-viewer-pause">'+PLAY_ICON+'</span>'
|
|
626
|
+
+'<div class="rk-discover-viewer-shade"></div>'
|
|
627
|
+
+ bmBtnHtml(id,'rk-discover-viewer-bm')
|
|
628
|
+
+'<div class="rk-discover-viewer-info">'
|
|
629
|
+
+(meta.length?'<div class="rk-discover-viewer-meta">'+meta.join(' · ')+'</div>':'')
|
|
630
|
+
+'<div class="rk-discover-viewer-title">'+esc(t.title||id)+'</div>'
|
|
631
|
+
+(desc?'<div class="rk-discover-viewer-desc">'+esc(desc)+'</div>':'')
|
|
632
|
+
+'<a class="rk-btn rk-btn-gold rk-btn-sm rk-discover-viewer-fork" href="'+attr(editorUrl(id))+'">'+EDIT_ICON+' Open Editor</a>'
|
|
633
|
+
+'</div>'
|
|
634
|
+
+'</figure>'
|
|
635
|
+
+'</section>';
|
|
636
|
+
}
|
|
637
|
+
function hydrateSlide(slide){
|
|
638
|
+
var v=slide.querySelector('video[data-vsrc]');
|
|
639
|
+
if(!v||v.getAttribute('src')) return;
|
|
640
|
+
v.preload='auto';
|
|
641
|
+
v.setAttribute('src', v.getAttribute('data-vsrc'));
|
|
642
|
+
}
|
|
643
|
+
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
644
|
+
function setViewerActive(idx){
|
|
645
|
+
vActive=idx;
|
|
646
|
+
[].forEach.call(vTrack.children,function(s,i){
|
|
647
|
+
if(Math.abs(i-idx)<=1) hydrateSlide(s);
|
|
648
|
+
s.classList.remove('is-paused');
|
|
649
|
+
var v=s.querySelector('video');
|
|
650
|
+
if(!v) return;
|
|
651
|
+
if(i===idx){
|
|
652
|
+
v.muted=!vSound;
|
|
653
|
+
var pr=v.play();
|
|
654
|
+
if(pr&&pr.catch) pr.catch(function(){
|
|
655
|
+
// sound-on autoplay blocked → fall back to muted playback
|
|
656
|
+
v.muted=true; vSound=false; updateMuteUi();
|
|
657
|
+
var pr2=v.play(); if(pr2&&pr2.catch) pr2.catch(function(){});
|
|
658
|
+
});
|
|
659
|
+
} else { v.pause(); }
|
|
660
|
+
});
|
|
661
|
+
updateMuteUi();
|
|
662
|
+
}
|
|
663
|
+
function stepViewer(d){
|
|
664
|
+
if(!vOpen) return;
|
|
665
|
+
var next=Math.max(0,Math.min(vItems.length-1,vActive+d));
|
|
666
|
+
if(next===vActive) return;
|
|
667
|
+
vTrack.scrollTo({top:next*vTrack.clientHeight,behavior:'smooth'});
|
|
668
|
+
}
|
|
669
|
+
function open(list, clickedIdx, park){
|
|
670
|
+
vItems=[]; var start=0;
|
|
671
|
+
(list||[]).forEach(function(t,i){
|
|
672
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
673
|
+
if(!url||t.status==='processing'||t.status==='failed') return;
|
|
674
|
+
if(i===clickedIdx) start=vItems.length;
|
|
675
|
+
vItems.push(t);
|
|
676
|
+
});
|
|
677
|
+
if(!vItems.length) return;
|
|
678
|
+
parkEl=park||null;
|
|
679
|
+
vTrack.innerHTML=vItems.map(viewerSlideHtml).join('');
|
|
680
|
+
vOpen=true; vSound=true;
|
|
681
|
+
viewer.hidden=false;
|
|
682
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
683
|
+
parkVideos(parkEl);
|
|
684
|
+
if(vIO) vIO.disconnect();
|
|
685
|
+
vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){
|
|
686
|
+
entries.forEach(function(en){
|
|
687
|
+
if(!en.isIntersecting) return;
|
|
688
|
+
var i=Number(en.target.getAttribute('data-vslide'));
|
|
689
|
+
if(i!==vActive) setViewerActive(i);
|
|
690
|
+
});
|
|
691
|
+
},{root:vTrack,threshold:0.6}):null;
|
|
692
|
+
if(vIO) [].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
693
|
+
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
694
|
+
setViewerActive(start);
|
|
695
|
+
}
|
|
696
|
+
function close(){
|
|
697
|
+
if(!vOpen) return;
|
|
698
|
+
vOpen=false; vActive=-1;
|
|
699
|
+
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
700
|
+
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
701
|
+
vTrack.innerHTML='';
|
|
702
|
+
viewer.hidden=true;
|
|
703
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
704
|
+
resumeVideos(parkEl);
|
|
705
|
+
parkEl=null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
709
|
+
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
710
|
+
if(muteBtn) muteBtn.addEventListener('click',function(){
|
|
711
|
+
vSound=!vSound; updateMuteUi();
|
|
712
|
+
var s=vTrack.children[vActive]; var v=s&&s.querySelector('video');
|
|
713
|
+
if(v){ v.muted=!vSound; if(vSound&&v.paused){ var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){}); s.classList.remove('is-paused'); } }
|
|
714
|
+
});
|
|
715
|
+
var prevBtn=viewer.querySelector('[data-viewer-prev]');
|
|
716
|
+
var nextBtn=viewer.querySelector('[data-viewer-next]');
|
|
717
|
+
if(prevBtn) prevBtn.addEventListener('click',function(){ stepViewer(-1); });
|
|
718
|
+
if(nextBtn) nextBtn.addEventListener('click',function(){ stepViewer(1); });
|
|
719
|
+
// tap the video → pause/resume (TikTok); tap the empty gutter → close;
|
|
720
|
+
// bookmarks are handled by the document-level [data-bm] listener
|
|
721
|
+
vTrack.addEventListener('click',function(e){
|
|
722
|
+
var t=e.target;
|
|
723
|
+
if(t&&t.closest&&(t.closest('a')||t.closest('[data-bm]'))) return;
|
|
724
|
+
var media=(t&&t.closest)?t.closest('.rk-discover-viewer-media'):null;
|
|
725
|
+
if(media&&media.tagName==='VIDEO'){
|
|
726
|
+
var slide=media.closest('.rk-discover-viewer-slide');
|
|
727
|
+
if(media.paused){ var pr=media.play(); if(pr&&pr.catch) pr.catch(function(){}); if(slide) slide.classList.remove('is-paused'); }
|
|
728
|
+
else { media.pause(); if(slide) slide.classList.add('is-paused'); }
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if(t&&t.classList&&(t.classList.contains('rk-discover-viewer-slide')||t===vTrack)) close();
|
|
732
|
+
});
|
|
733
|
+
document.addEventListener('keydown',function(e){
|
|
734
|
+
if(!vOpen) return;
|
|
735
|
+
if(e.key==='Escape'){ e.preventDefault(); close(); }
|
|
736
|
+
else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); stepViewer(1); }
|
|
737
|
+
else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); stepViewer(-1); }
|
|
738
|
+
});
|
|
739
|
+
window.addEventListener('resize',function(){
|
|
740
|
+
if(vOpen&&vActive>=0) vTrack.scrollTop=vActive*vTrack.clientHeight;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
return { open: open, close: close, isOpen: function(){ return vOpen; } };
|
|
744
|
+
})();
|
|
745
|
+
|
|
746
|
+
// Wire a poster grid's plain-clicks into the shared viewer. getList is
|
|
747
|
+
// called at click time so it always reflects the grid's CURRENT contents
|
|
748
|
+
// (search filter, bookmark filter, revealed group).
|
|
749
|
+
function bindViewerOpens(container, getList){
|
|
750
|
+
if(!container||!viewerCtl) return;
|
|
751
|
+
container.addEventListener('click',function(e){
|
|
752
|
+
if(e.defaultPrevented||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0) return;
|
|
753
|
+
var a=(e.target&&e.target.closest)?e.target.closest('[data-rk-view]'):null;
|
|
754
|
+
if(!a||!container.contains(a)) return;
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
viewerCtl.open(getList()||[], Number(a.getAttribute('data-rk-view')), container);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
761
|
+
// FEED
|
|
762
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
763
|
+
var feedRoot=root.querySelector('[data-rk-feed]');
|
|
764
|
+
if(feedRoot){
|
|
765
|
+
var gridEl=feedRoot.querySelector('[data-rk-grid]');
|
|
766
|
+
var stateEl=feedRoot.querySelector('[data-rk-state]');
|
|
767
|
+
// search + count live in the shared toolbar row (outside [data-rk-feed])
|
|
768
|
+
var countEl=root.querySelector('[data-rk-count]');
|
|
769
|
+
var searchEl=root.querySelector('[data-rk-search]');
|
|
770
|
+
var bmChip=root.querySelector('[data-rk-bmfilter]');
|
|
771
|
+
var bmCountEl=root.querySelector('[data-rk-bmcount]');
|
|
772
|
+
var bmFilter=false;
|
|
773
|
+
var feedState={items:[],visible:[],loading:false};
|
|
774
|
+
var feedReq=0;
|
|
775
|
+
|
|
776
|
+
function feedSetState(msg,isError){
|
|
777
|
+
if(gridEl) gridEl.innerHTML='';
|
|
778
|
+
if(stateEl){ stateEl.hidden=false; stateEl.innerHTML=msg; stateEl.classList.toggle('is-error',!!isError); }
|
|
779
|
+
if(countEl) countEl.textContent='';
|
|
780
|
+
}
|
|
781
|
+
function updateBmChip(){
|
|
782
|
+
if(!bmChip) return;
|
|
783
|
+
var n=0;
|
|
784
|
+
feedState.items.forEach(function(t){ if(isBookmarked(t.templateId||t.template_id)) n++; });
|
|
785
|
+
if(bmCountEl) bmCountEl.textContent=n?String(n):'';
|
|
786
|
+
bmChip.classList.toggle('is-active',bmFilter);
|
|
787
|
+
bmChip.setAttribute('aria-pressed',bmFilter?'true':'false');
|
|
788
|
+
}
|
|
789
|
+
function feedRender(){
|
|
790
|
+
var vis=bmFilter
|
|
791
|
+
? feedState.items.filter(function(t){ return isBookmarked(t.templateId||t.template_id); })
|
|
792
|
+
: feedState.items;
|
|
793
|
+
feedState.visible=vis;
|
|
794
|
+
updateBmChip();
|
|
795
|
+
if(!vis.length){
|
|
796
|
+
feedSetState(bmFilter?'No bookmarks yet — tap the bookmark on any template to save it for later.':'No templates match — try a different search.',false);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if(stateEl) stateEl.hidden=true;
|
|
800
|
+
if(gridEl) gridEl.innerHTML=vis.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
801
|
+
if(countEl) countEl.textContent=vis.length+' template'+(vis.length===1?'':'s');
|
|
802
|
+
}
|
|
803
|
+
if(bmChip) bmChip.addEventListener('click',function(){ bmFilter=!bmFilter; feedRender(); });
|
|
804
|
+
bmChangeHandlers.push(function(){
|
|
805
|
+
updateBmChip();
|
|
806
|
+
// with the filter on, un-bookmarking drops the card from the grid; the
|
|
807
|
+
// re-render restarts muted autoplay, so re-park it if the viewer is open
|
|
808
|
+
if(bmFilter){ feedRender(); if(viewerCtl&&viewerCtl.isOpen()) parkVideos(gridEl); }
|
|
809
|
+
});
|
|
810
|
+
function feedLoad(query){
|
|
811
|
+
var my=++feedReq;
|
|
812
|
+
feedState.loading=true;
|
|
813
|
+
feedSetState('Loading templates…',false);
|
|
814
|
+
var url=feedEndpoint;
|
|
815
|
+
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
|
+
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
817
|
+
.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; feedState.items=(j&&Array.isArray(j.templates))?j.templates:[]; feedState.loading=false; feedRender(); })
|
|
819
|
+
.catch(function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); });
|
|
820
|
+
}
|
|
821
|
+
// Honor a ?q= (e.g. from a category link) in the page URL.
|
|
822
|
+
var initialQuery='';
|
|
823
|
+
try{ initialQuery=(new URL(window.location.href)).searchParams.get('q')||''; }catch(e){ initialQuery=''; }
|
|
824
|
+
if(searchEl){
|
|
825
|
+
searchEl.value=initialQuery;
|
|
826
|
+
var debounce=null;
|
|
827
|
+
searchEl.addEventListener('input',function(){
|
|
828
|
+
if(debounce) window.clearTimeout(debounce);
|
|
829
|
+
var v=searchEl.value.trim();
|
|
830
|
+
debounce=window.setTimeout(function(){ feedLoad(v); },260);
|
|
831
|
+
});
|
|
832
|
+
searchEl.addEventListener('keydown',function(e){ if(e.key==='Enter'){ if(debounce) window.clearTimeout(debounce); feedLoad(searchEl.value.trim()); } });
|
|
833
|
+
}
|
|
834
|
+
bindHoverAudio(gridEl);
|
|
835
|
+
// plain click on a card → shared viewer over the CURRENT visible feed
|
|
836
|
+
// (search + bookmark filter applied)
|
|
837
|
+
bindViewerOpens(gridEl, function(){ return feedState.visible; });
|
|
838
|
+
feedLoad(initialQuery.trim());
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
842
|
+
// SWIPE
|
|
843
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
844
|
+
var swipeRoot=root.querySelector('[data-rk-swipe]');
|
|
845
|
+
if(swipeRoot){
|
|
846
|
+
var deck=swipeRoot.querySelector('[data-deck]');
|
|
847
|
+
var swStateEl=swipeRoot.querySelector('[data-rk-swipe-state]');
|
|
848
|
+
var actionBtns=[].slice.call(swipeRoot.querySelectorAll('[data-swipe-act]'));
|
|
849
|
+
var hint=swipeRoot.querySelector('[data-actions-hint]');
|
|
850
|
+
var skipBtn=swipeRoot.querySelector('[data-swipe-skip]');
|
|
851
|
+
var forkBtn=swipeRoot.querySelector('[data-swipe-fork]');
|
|
852
|
+
var sw={target:5,customizing:0,productSet:false,busy:false,refilling:false,loaded:false};
|
|
853
|
+
var nodes=[]; // live card DOM nodes, top-of-deck first
|
|
854
|
+
|
|
855
|
+
function swShowState(html,kind){
|
|
856
|
+
if(swStateEl){ swStateEl.hidden=false; swStateEl.innerHTML=html; swStateEl.classList.toggle('is-error',kind==='error'); }
|
|
857
|
+
actionBtns.forEach(function(b){ b.hidden=true; });
|
|
858
|
+
if(hint) hint.hidden=true;
|
|
859
|
+
}
|
|
860
|
+
function swHideState(){ if(swStateEl) swStateEl.hidden=true; }
|
|
861
|
+
|
|
862
|
+
function makeCard(card){
|
|
863
|
+
var el=document.createElement('article');
|
|
864
|
+
el.className='rk-discover-swipe';
|
|
865
|
+
el._card=card; el._iframed=false;
|
|
866
|
+
var title=esc(card.title||card.source_title||card.template_id||'Template');
|
|
867
|
+
var caption=card.caption?('<div class="rk-discover-swipe-caption">'+esc(card.caption)+'</div>'):'';
|
|
868
|
+
el.innerHTML='<div class="rk-discover-swipe-fallback"><span>'+title+'</span></div>'
|
|
869
|
+
+'<div class="rk-discover-swipe-scrim"></div>'
|
|
870
|
+
+'<div class="rk-discover-swipe-info"><div class="rk-discover-swipe-title">'+title+'</div>'+caption+'</div>';
|
|
871
|
+
deck.appendChild(el);
|
|
872
|
+
return el;
|
|
873
|
+
}
|
|
874
|
+
function upgradeToIframe(el){
|
|
875
|
+
if(el._iframed) return;
|
|
876
|
+
var card=el._card; var url=card&&(card.preview_url||card.previewUrl);
|
|
877
|
+
if(!url) return;
|
|
878
|
+
var fb=el.querySelector('.rk-discover-swipe-fallback');
|
|
879
|
+
var frame=document.createElement('iframe');
|
|
880
|
+
frame.className='rk-discover-swipe-frame';
|
|
881
|
+
frame.setAttribute('src', url);
|
|
882
|
+
frame.setAttribute('title', String(card.title||'Preview'));
|
|
883
|
+
frame.setAttribute('loading','lazy');
|
|
884
|
+
frame.setAttribute('scrolling','no');
|
|
885
|
+
frame.setAttribute('allow','autoplay');
|
|
886
|
+
if(fb&&fb.parentNode) fb.parentNode.insertBefore(frame, fb); else el.insertBefore(frame, el.firstChild);
|
|
887
|
+
el._iframed=true;
|
|
888
|
+
}
|
|
329
889
|
function restack(){
|
|
330
|
-
|
|
331
|
-
if(i>2){
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
890
|
+
nodes.forEach(function(el,i){
|
|
891
|
+
if(i>2){ el.style.display='none'; return; }
|
|
892
|
+
el.style.display='';
|
|
893
|
+
el.style.zIndex=String(20-i);
|
|
894
|
+
el.style.transform='translateY('+(i*12)+'px) scale('+(1-i*0.045).toFixed(3)+')';
|
|
895
|
+
el.style.opacity=i===0?'1':(i===1?'0.92':'0.82');
|
|
896
|
+
if(i<=1) upgradeToIframe(el);
|
|
337
897
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
898
|
+
var has=nodes.length>0;
|
|
899
|
+
actionBtns.forEach(function(b){ b.hidden=!has; });
|
|
900
|
+
if(hint) hint.hidden=!has;
|
|
901
|
+
if(!has) swCaughtUp();
|
|
902
|
+
}
|
|
903
|
+
function swCaughtUp(){
|
|
904
|
+
if(sw.customizing>0||sw.refilling){
|
|
905
|
+
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div class="rk-discover-swipe-state-title">Customizing fresh clips…</div><div>Recutting viral templates into ads for your product.</div>');
|
|
906
|
+
} else {
|
|
907
|
+
swShowState('<div class="rk-discover-swipe-state-title">You\\u2019re all caught up</div><div>No more templates to customize right now. Check back as new ones are added.</div>');
|
|
342
908
|
}
|
|
343
909
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
910
|
+
function decide(dir){
|
|
911
|
+
if(sw.busy||!nodes.length) return;
|
|
912
|
+
sw.busy=true;
|
|
913
|
+
var el=nodes.shift();
|
|
914
|
+
var card=el._card;
|
|
915
|
+
el.classList.remove('is-front');
|
|
916
|
+
el.style.zIndex='30';
|
|
917
|
+
el.style.transform='translateX('+(dir==='fork'?460:-460)+'px) rotate('+(dir==='fork'?15:-15)+'deg)';
|
|
918
|
+
el.style.opacity='0';
|
|
919
|
+
window.setTimeout(function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); },380);
|
|
920
|
+
var path=swipeBase+'/'+encodeURIComponent(String(card.id))+'/'+(dir==='fork'?'approve':'reject');
|
|
921
|
+
fetch(withAccount(path),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
922
|
+
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
923
|
+
.catch(function(){ /* swallowed — the card already left the deck */ })
|
|
924
|
+
.then(function(){ sw.busy=false; restack(); refill(); });
|
|
925
|
+
}
|
|
926
|
+
function refill(){
|
|
927
|
+
if(sw.refilling||!sw.productSet||!sw.loaded) return;
|
|
928
|
+
if(nodes.length>=sw.target) return;
|
|
929
|
+
sw.refilling=true;
|
|
930
|
+
fetch(withAccount(swipeRefillEndpoint),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
931
|
+
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
932
|
+
.then(function(j){
|
|
933
|
+
sw.refilling=false;
|
|
934
|
+
if(!j||j.ok===false) return; // e.g. need_provider_key — stop quietly
|
|
935
|
+
if(j.filled&&j.record){ makeCard(j.record); restack(); }
|
|
936
|
+
var more=j.filled&&nodes.length<sw.target&&j.reason!=='no_eligible'&&j.reason!=='at_target';
|
|
937
|
+
if(more) refill();
|
|
938
|
+
})
|
|
939
|
+
.catch(function(){ sw.refilling=false; });
|
|
352
940
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
941
|
+
function loadStack(){
|
|
942
|
+
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div>Loading your deck…</div>');
|
|
943
|
+
fetch(withAccount(swipeStackEndpoint),{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
944
|
+
.then(function(r){
|
|
945
|
+
if(r.status===401){ var e=new Error('unauth'); e.code=401; throw e; }
|
|
946
|
+
if(!r.ok){ throw new Error("Couldn't load your deck ("+r.status+")."); }
|
|
947
|
+
return r.json();
|
|
948
|
+
})
|
|
949
|
+
.then(function(j){
|
|
950
|
+
sw.loaded=true;
|
|
951
|
+
sw.target=Number(j&&j.target_depth)||5;
|
|
952
|
+
sw.customizing=Number(j&&j.customizing)||0;
|
|
953
|
+
sw.productSet=Boolean(j&&j.product_description_set);
|
|
954
|
+
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
955
|
+
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#profile">Open Settings</a>');
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
nodes=stack.map(makeCard);
|
|
960
|
+
if(nodes.length){ swHideState(); restack(); }
|
|
961
|
+
else { swCaughtUp(); }
|
|
962
|
+
refill();
|
|
963
|
+
})
|
|
964
|
+
.catch(function(err){
|
|
965
|
+
if(err&&err.code===401){
|
|
966
|
+
swShowState('<div class="rk-discover-swipe-state-title">Swipe is for your product</div><div>Log in to auto-generate ads from viral templates and swipe to approve.</div><a class="rk-btn rk-btn-gold rk-btn-sm" href="/login">Log in</a>');
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
swShowState('<div class="rk-discover-swipe-state-title">Couldn\\u2019t load your deck</div><div>'+esc(friendlyError(err))+'</div>','error');
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if(skipBtn) skipBtn.addEventListener('click',function(){ decide('skip'); });
|
|
973
|
+
if(forkBtn) forkBtn.addEventListener('click',function(){ decide('fork'); });
|
|
974
|
+
loadStack();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
978
|
+
// CATEGORIES
|
|
979
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
980
|
+
var catsRoot=root.querySelector('[data-rk-cats]');
|
|
981
|
+
if(catsRoot){
|
|
982
|
+
var catsGrid=catsRoot.querySelector('[data-rk-cats-grid]');
|
|
983
|
+
var reveal=catsRoot.querySelector('[data-rk-cat-reveal]');
|
|
984
|
+
var revealGrid=catsRoot.querySelector('[data-rk-reveal-grid]');
|
|
985
|
+
// reveal header ("← All categories" + group title) lives in the toolbar row
|
|
986
|
+
var revealHead=root.querySelector('[data-rk-reveal-head]');
|
|
987
|
+
var revealTitle=root.querySelector('[data-rk-reveal-title]');
|
|
988
|
+
var backBtn=root.querySelector('[data-rk-cat-back]');
|
|
989
|
+
var catsState=catsRoot.querySelector('[data-rk-cats-state]');
|
|
990
|
+
var groups={}; // label -> [templates]
|
|
991
|
+
var revealItems=[]; // the currently-revealed group's templates (viewer list)
|
|
992
|
+
|
|
993
|
+
function miniHtml(t){
|
|
994
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
995
|
+
if(!url) return '<span class="rk-discover-mini rk-discover-mini-ph"></span>';
|
|
996
|
+
if(isImage(url)) return '<span class="rk-discover-mini"><img src="'+attr(url)+'" alt="" loading="lazy" decoding="async"></span>';
|
|
997
|
+
// First-frame still, NOT autoplay: 4 tiles x 16 category cards would be
|
|
998
|
+
// ~64 playing videos. preload=metadata + #t=0.1 paints frame one only.
|
|
999
|
+
return '<span class="rk-discover-mini"><video src="'+attr(url+'#t=0.1')+'" muted playsinline preload="metadata" tabindex="-1" aria-hidden="true"></video></span>';
|
|
1000
|
+
}
|
|
1001
|
+
function showReveal(label){
|
|
1002
|
+
var items=groups[label]||[];
|
|
1003
|
+
if(!items.length) return false;
|
|
1004
|
+
revealItems=items;
|
|
1005
|
+
if(revealTitle) revealTitle.textContent=label;
|
|
1006
|
+
// cards carry data-rk-view=<i> so a plain click opens the shared viewer
|
|
1007
|
+
if(revealGrid) revealGrid.innerHTML=items.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
1008
|
+
if(catsGrid) catsGrid.hidden=true;
|
|
1009
|
+
if(reveal) reveal.hidden=false;
|
|
1010
|
+
if(revealHead) revealHead.hidden=false;
|
|
1011
|
+
window.scrollTo(0,0);
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
function hideReveal(){
|
|
1015
|
+
if(viewerCtl&&viewerCtl.isOpen()) viewerCtl.close();
|
|
1016
|
+
revealItems=[];
|
|
1017
|
+
if(reveal) reveal.hidden=true;
|
|
1018
|
+
if(revealHead) revealHead.hidden=true;
|
|
1019
|
+
if(catsGrid) catsGrid.hidden=false;
|
|
1020
|
+
}
|
|
1021
|
+
if(backBtn) backBtn.addEventListener('click',hideReveal);
|
|
1022
|
+
bindHoverAudio(revealGrid); // reveal cards share posterCardHtml → same hover audio
|
|
1023
|
+
bindViewerOpens(revealGrid, function(){ return revealItems; }); // click → shared TikTok viewer
|
|
1024
|
+
|
|
1025
|
+
// Intercept category-card clicks: if we have live templates for that group,
|
|
1026
|
+
// reveal them in-page; otherwise let the href fall through to a filtered
|
|
1027
|
+
// feed (graceful offline / not-yet-seeded fallback).
|
|
1028
|
+
catsRoot.querySelectorAll('[data-rk-cat]').forEach(function(a){
|
|
1029
|
+
a.addEventListener('click',function(e){
|
|
1030
|
+
var label=a.getAttribute('data-rk-cat');
|
|
1031
|
+
if(groups[label]&&groups[label].length){ e.preventDefault(); showReveal(label); }
|
|
1032
|
+
});
|
|
368
1033
|
});
|
|
1034
|
+
|
|
1035
|
+
function enrichCards(){
|
|
1036
|
+
catsRoot.querySelectorAll('[data-rk-cat]').forEach(function(a){
|
|
1037
|
+
var label=a.getAttribute('data-rk-cat');
|
|
1038
|
+
var items=groups[label]||[];
|
|
1039
|
+
if(!items.length) return;
|
|
1040
|
+
var thumbs=a.querySelector('[data-rk-cat-thumbs]');
|
|
1041
|
+
if(thumbs) thumbs.innerHTML=items.slice(0,4).map(miniHtml).join('');
|
|
1042
|
+
var count=a.querySelector('[data-rk-cat-count]');
|
|
1043
|
+
if(count) count.textContent=items.length+' template'+(items.length===1?'':'s');
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
function ingestPopular(list){
|
|
1047
|
+
groups={};
|
|
1048
|
+
(list||[]).forEach(function(t){ var g=(t.popularGroup||'Popular').trim()||'Popular'; (groups[g]=groups[g]||[]).push(t); });
|
|
1049
|
+
enrichCards();
|
|
1050
|
+
}
|
|
1051
|
+
function loadPopular(){
|
|
1052
|
+
var url=popularEndpoint;
|
|
1053
|
+
try{ var u=new URL(popularEndpoint, window.location.href); if(accountId) u.searchParams.set('account', accountId); url=u.toString(); }catch(e){ url=popularEndpoint; }
|
|
1054
|
+
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1055
|
+
.then(function(r){ if(!r.ok){ throw new Error("Couldn't load categories ("+r.status+")."); } return r.json(); })
|
|
1056
|
+
.then(function(j){
|
|
1057
|
+
var list=(j&&Array.isArray(j.templates))?j.templates:[];
|
|
1058
|
+
if(list.length){ ingestPopular(list); return; }
|
|
1059
|
+
// Local serve without the popular catalog seeded (and no upstream
|
|
1060
|
+
// passthrough configured): fall back to the public prod endpoint —
|
|
1061
|
+
// it's CORS-open and unauthenticated — so the sandbox still shows
|
|
1062
|
+
// real first-frame previews on a fully-local box.
|
|
1063
|
+
loadPopularFallback();
|
|
1064
|
+
// The static skeleton already shows every real group; the fetch just
|
|
1065
|
+
// enriches it, so a fetch that returns nothing is not an error state.
|
|
1066
|
+
})
|
|
1067
|
+
.catch(function(){
|
|
1068
|
+
// Offline / unreachable: try the public prod catalog before settling
|
|
1069
|
+
// for the static skeleton grid (real group labels + counts, clicks
|
|
1070
|
+
// fall through to the filtered feed).
|
|
1071
|
+
loadPopularFallback();
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
function loadPopularFallback(){
|
|
1075
|
+
if(window.location.hostname==='vidfarm.cc') return; // already prod
|
|
1076
|
+
fetch('https://vidfarm.cc/discover/popular',{headers:{'accept':'application/json'}})
|
|
1077
|
+
.then(function(r){ return r.ok?r.json():null; })
|
|
1078
|
+
.then(function(j){
|
|
1079
|
+
var list=(j&&Array.isArray(j.templates))?j.templates:[];
|
|
1080
|
+
if(list.length) ingestPopular(list);
|
|
1081
|
+
})
|
|
1082
|
+
.catch(function(){ /* keep the static skeleton */ });
|
|
1083
|
+
}
|
|
1084
|
+
loadPopular();
|
|
369
1085
|
}
|
|
370
1086
|
})();`;
|
|
371
1087
|
return reskinDocument({
|
|
372
1088
|
title: `vidfarm reskin — Discover (${mode})`,
|
|
373
|
-
description: "Reskinned vidfarm Discover — Feed / Swipe / Categories
|
|
1089
|
+
description: "Reskinned vidfarm Discover — real Feed / Swipe / Categories, live template posters, warm sunny SaaS.",
|
|
374
1090
|
activeSlug: "discover",
|
|
375
1091
|
pageCss,
|
|
376
1092
|
body,
|