@mevdragon/vidfarm-devcli 0.17.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 +43 -6
- package/SKILL.director.md +37 -23
- package/SKILL.platform.md +6 -6
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1832 -213
- package/dist/src/cli.js +238 -17
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +116 -73
- 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 +214 -170
- package/dist/src/reskin/calendar-page.js +503 -187
- package/dist/src/reskin/chat-page.js +739 -299
- package/dist/src/reskin/discover-page.js +1450 -279
- package/dist/src/reskin/document.js +1392 -16
- package/dist/src/reskin/help-page.js +139 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +547 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +1188 -317
- package/dist/src/reskin/login-page.js +124 -87
- package/dist/src/reskin/pricing-page.js +197 -166
- package/dist/src/reskin/settings-page.js +566 -187
- package/dist/src/reskin/theme.js +434 -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
|
@@ -1,17 +1,58 @@
|
|
|
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
|
+
/** Config-only defaults (the data fields feedInitial/feedFetchedAt are seeded
|
|
45
|
+
* per-request, never defaulted). */
|
|
46
|
+
const DEFAULT_INPUT = {
|
|
47
|
+
accountId: "",
|
|
48
|
+
isLoggedIn: false,
|
|
49
|
+
feedEndpoint: "/discover/feed",
|
|
50
|
+
popularEndpoint: "/discover/popular",
|
|
51
|
+
swipeStackEndpoint: "/discover/swipe/stack",
|
|
52
|
+
swipeRefillEndpoint: "/discover/swipe/refill",
|
|
53
|
+
swipeBase: "/discover/swipe",
|
|
54
|
+
editorBase: "/editor"
|
|
55
|
+
};
|
|
15
56
|
const MODES = [
|
|
16
57
|
{ mode: "feed", label: "Feed" },
|
|
17
58
|
{ mode: "swipe", label: "Swipe" },
|
|
@@ -19,227 +60,278 @@ const MODES = [
|
|
|
19
60
|
];
|
|
20
61
|
const MODE_COPY = {
|
|
21
62
|
feed: {
|
|
22
|
-
|
|
23
|
-
sub: "Browse proven short-form formats, then fork one into a live editor and make it yours in an afternoon."
|
|
63
|
+
sub: "Browse proven short-form formats, then open one in the editor and make it yours in an afternoon."
|
|
24
64
|
},
|
|
25
65
|
swipe: {
|
|
26
|
-
badge: "Swipe to customize",
|
|
27
66
|
sub: "One template at a time — keep the ones that fit and we'll recut them into ads for your brand."
|
|
28
67
|
},
|
|
29
68
|
categories: {
|
|
30
|
-
badge: "Browse by format",
|
|
31
69
|
sub: "Jump straight to the style you need — every short-form format, neatly grouped."
|
|
32
70
|
}
|
|
33
71
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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>`;
|
|
72
|
+
// Functional (non-decorative) SVG glyphs.
|
|
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>`;
|
|
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>`;
|
|
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>`;
|
|
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>`;
|
|
79
|
+
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>`;
|
|
80
|
+
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>`;
|
|
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>`;
|
|
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>`;
|
|
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>`;
|
|
86
|
+
/** Safe JSON embedding inside a <script> tag (mirrors page-shell.escapeJsonForHtml). */
|
|
87
|
+
function jsonForScript(value) {
|
|
88
|
+
return JSON.stringify(value ?? null)
|
|
89
|
+
.replace(/</g, "\\u003c")
|
|
90
|
+
.replace(/>/g, "\\u003e")
|
|
91
|
+
.replace(/&/g, "\\u0026")
|
|
92
|
+
.replace(/\u2028/g, "\\u2028")
|
|
93
|
+
.replace(/\u2029/g, "\\u2029");
|
|
145
94
|
}
|
|
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>`;
|
|
95
|
+
// The segmented Feed / Swipe / Categories toggle (three real links).
|
|
96
|
+
function modeToggle(active) {
|
|
97
|
+
const links = MODES.map((m) => {
|
|
98
|
+
const on = m.mode === active ? " is-active" : "";
|
|
99
|
+
return `<a class="rk-discover-mode${on}" href="/discover/${m.mode}"${m.mode === active ? ' aria-current="page"' : ""}>${m.label}</a>`;
|
|
100
|
+
}).join("");
|
|
101
|
+
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
167
102
|
}
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
103
|
+
// Server-rendered category skeleton from the real POPULAR_INSPIRATION_GROUPS
|
|
104
|
+
// manifest — always renders (real group labels + real format counts) even
|
|
105
|
+
// before/without the /discover/popular fetch, so categories mode is never blank
|
|
106
|
+
// and degrades gracefully offline. The client enriches each card with real
|
|
107
|
+
// thumbnails + wires an in-page reveal of that group's real templates.
|
|
108
|
+
function categorySkeleton() {
|
|
109
|
+
return POPULAR_INSPIRATION_GROUPS.map((g) => {
|
|
110
|
+
const count = g.items.length;
|
|
111
|
+
const blurb = g.items.slice(0, 4).map((i) => i.title).join(" · ");
|
|
112
|
+
const fallbackQuery = encodeURIComponent(g.label);
|
|
113
|
+
// Placeholder thumb tiles (no media yet) keep the card height stable; the
|
|
114
|
+
// client swaps in real poster media once /discover/popular resolves.
|
|
115
|
+
const thumbs = Array.from({ length: 4 })
|
|
116
|
+
.map(() => `<span class="rk-discover-mini rk-discover-mini-ph"></span>`)
|
|
117
|
+
.join("");
|
|
118
|
+
return `<a class="rk-discover-cat rk-card-hover" href="/discover?q=${fallbackQuery}" data-rk-cat="${rkEscape(g.label)}">
|
|
174
119
|
<div class="rk-discover-cat-head">
|
|
175
120
|
<div class="rk-discover-cat-titlewrap">
|
|
176
|
-
<h3 class="rk-discover-cat-title">${g.label}</h3>
|
|
177
|
-
<p class="rk-discover-cat-count">${
|
|
121
|
+
<h3 class="rk-discover-cat-title">${rkEscape(g.label)}</h3>
|
|
122
|
+
<p class="rk-discover-cat-count" data-rk-cat-count>${count} format${count === 1 ? "" : "s"}</p>
|
|
178
123
|
</div>
|
|
179
|
-
<span class="rk-discover-cat-arrow" aria-hidden="true"
|
|
124
|
+
<span class="rk-discover-cat-arrow" aria-hidden="true">→</span>
|
|
180
125
|
</div>
|
|
181
|
-
<
|
|
182
|
-
<
|
|
126
|
+
<div class="rk-discover-cat-thumbs" data-rk-cat-thumbs>${thumbs}</div>
|
|
127
|
+
<p class="rk-discover-cat-blurb">${rkEscape(blurb)}</p>
|
|
183
128
|
</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
129
|
}).join("");
|
|
191
|
-
return `<nav class="rk-discover-modes" aria-label="Discover mode">${links}</nav>`;
|
|
192
130
|
}
|
|
193
|
-
|
|
131
|
+
// The fullscreen TikTok-style viewer shell — shared by feed and categories
|
|
132
|
+
// (the client script fills [data-viewer-track] with slides on open).
|
|
133
|
+
function viewerHtml() {
|
|
134
|
+
return `
|
|
135
|
+
<div class="rk-discover-viewer" data-rk-viewer hidden role="dialog" aria-modal="true" aria-label="Template viewer">
|
|
136
|
+
<div class="rk-discover-viewer-scrim" data-viewer-close></div>
|
|
137
|
+
<div class="rk-discover-viewer-track" data-viewer-track></div>
|
|
138
|
+
<div class="rk-discover-viewer-ui">
|
|
139
|
+
<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>
|
|
140
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="rk-discover-viewer-nav">
|
|
143
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-prev aria-label="Previous template">${CHEV_UP_ICON}</button>
|
|
144
|
+
<button type="button" class="rk-discover-viewer-btn" data-viewer-next aria-label="Next template">${CHEV_DOWN_ICON}</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>`;
|
|
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
|
+
}
|
|
190
|
+
export function renderReskinDiscover(mode = "feed", input = {}) {
|
|
194
191
|
const copy = MODE_COPY[mode];
|
|
195
|
-
//
|
|
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
|
+
};
|
|
196
201
|
let modeBody = "";
|
|
202
|
+
// ── FEED ────────────────────────────────────────────────────────────────
|
|
197
203
|
if (mode === "feed") {
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
modeBody = `
|
|
205
|
+
<div class="rk-discover-feed" data-rk-feed>
|
|
206
|
+
<div class="rk-discover-grid" data-rk-grid></div>
|
|
207
|
+
<div class="rk-discover-state" data-rk-state>Loading templates…</div>
|
|
208
|
+
</div>${viewerHtml()}`;
|
|
200
209
|
}
|
|
201
|
-
//
|
|
210
|
+
// Feed's quick search + bookmarked-only chip share the toolbar row with the
|
|
211
|
+
// mode toggle.
|
|
212
|
+
const toolbarSearch = mode === "feed"
|
|
213
|
+
? `<label class="rk-discover-search">
|
|
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>
|
|
215
|
+
<input type="search" data-rk-search placeholder="Search templates" autocomplete="off" aria-label="Search templates" />
|
|
216
|
+
</label>
|
|
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>`
|
|
218
|
+
: "";
|
|
219
|
+
// Categories' in-page group reveal header ("← All categories" + the group
|
|
220
|
+
// title) lives IN the toolbar row, next to the mode toggle (user feedback);
|
|
221
|
+
// the client unhides it while a group is revealed.
|
|
222
|
+
const toolbarReveal = mode === "categories"
|
|
223
|
+
? `<div class="rk-discover-reveal-head" data-rk-reveal-head hidden>
|
|
224
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-cat-back>← All categories</button>
|
|
225
|
+
<h2 class="rk-discover-reveal-title" data-rk-reveal-title></h2>
|
|
226
|
+
</div>`
|
|
227
|
+
: "";
|
|
228
|
+
// ── SWIPE ───────────────────────────────────────────────────────────────
|
|
202
229
|
if (mode === "swipe") {
|
|
203
|
-
const deck = SWIPE_CARDS.map((c, i) => swipeCard(c, i)).join("");
|
|
204
230
|
modeBody = `
|
|
205
|
-
<div class="rk-discover-swipe-wrap">
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<
|
|
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>
|
|
231
|
+
<div class="rk-discover-swipe-wrap" data-rk-swipe>
|
|
232
|
+
<div class="rk-discover-stage">
|
|
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>
|
|
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>
|
|
236
|
+
<div class="rk-discover-swipe-state" data-rk-swipe-state>Loading your deck…</div>
|
|
213
237
|
</div>
|
|
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>
|
|
214
239
|
</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>
|
|
240
|
+
<p class="rk-discover-act-hint" data-actions-hint hidden>Swipe to customize into an ad for your brand.</p>
|
|
220
241
|
</div>`;
|
|
221
242
|
}
|
|
222
|
-
// ── CATEGORIES
|
|
243
|
+
// ── CATEGORIES ──────────────────────────────────────────────────────────
|
|
223
244
|
if (mode === "categories") {
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
modeBody = `
|
|
246
|
+
<div class="rk-discover-catwrap" data-rk-cats>
|
|
247
|
+
<div class="rk-discover-cats" data-rk-cats-grid>${categorySkeleton()}</div>
|
|
248
|
+
<section class="rk-discover-cat-reveal" data-rk-cat-reveal hidden>
|
|
249
|
+
<div class="rk-discover-grid" data-rk-reveal-grid></div>
|
|
250
|
+
</section>
|
|
251
|
+
<div class="rk-discover-state rk-discover-cats-state" data-rk-cats-state hidden></div>
|
|
252
|
+
</div>${viewerHtml()}`;
|
|
226
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
|
+
: "";
|
|
227
261
|
const body = `
|
|
228
262
|
<main class="rk-container rk-page">
|
|
229
263
|
<div class="rk-page-head rk-discover-head">
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
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}
|
|
269
|
+
</div>
|
|
270
|
+
<div class="rk-discover-toolbar">
|
|
271
|
+
${modeToggle(mode)}
|
|
272
|
+
${toolbarReveal}
|
|
273
|
+
${toolbarSearch}
|
|
233
274
|
</div>
|
|
234
|
-
${modeToggle(mode)}
|
|
235
275
|
${modeBody}
|
|
276
|
+
${mode === "feed" ? addModalHtml() : ""}
|
|
277
|
+
<script type="application/json" data-rk-discover-boot>${jsonForScript(boot)}</script>
|
|
236
278
|
</main>`;
|
|
237
279
|
const pageCss = `
|
|
238
|
-
|
|
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}
|
|
328
|
+
|
|
329
|
+
/* toolbar row: mode toggle + (feed) quick search share one line, wrap on mobile */
|
|
330
|
+
.rk-discover-toolbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:26px}
|
|
239
331
|
|
|
240
332
|
/* 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);
|
|
333
|
+
.rk-discover-modes{display:inline-flex;flex:none;gap:4px;background:var(--rk-n-100);border:1px solid var(--rk-border);
|
|
334
|
+
border-radius:var(--rk-r-full);padding:4px;box-shadow:var(--rk-shadow-xs);max-width:100%;overflow:auto;scrollbar-width:none}
|
|
243
335
|
.rk-discover-modes::-webkit-scrollbar{display:none}
|
|
244
336
|
.rk-discover-mode{display:inline-flex;align-items:center;gap:7px;padding:9px 18px;border-radius:var(--rk-r-full);
|
|
245
337
|
font-size:14px;font-weight:600;color:var(--rk-n-600);white-space:nowrap;
|
|
@@ -247,62 +339,209 @@ export function renderReskinDiscover(mode = "feed") {
|
|
|
247
339
|
.rk-discover-mode:hover{color:var(--rk-ink)}
|
|
248
340
|
.rk-discover-mode.is-active{background:var(--rk-gold-500);color:var(--rk-ink);font-weight:700;box-shadow:var(--rk-shadow-sm)}
|
|
249
341
|
|
|
250
|
-
/* ── feed: poster grid ── */
|
|
342
|
+
/* ── feed: quick search (lives in the toolbar row) + poster grid ── */
|
|
343
|
+
.rk-discover-search{flex:1;min-width:220px;display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--rk-border-strong);
|
|
344
|
+
border-radius:var(--rk-r-full);padding:10px 16px;box-shadow:var(--rk-shadow-xs);
|
|
345
|
+
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
346
|
+
.rk-discover-search:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
347
|
+
.rk-discover-search-ic{color:var(--rk-text-faint);display:grid;place-items:center;flex:none}
|
|
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)}
|
|
349
|
+
.rk-discover-search input::placeholder{color:var(--rk-n-400)}
|
|
350
|
+
|
|
251
351
|
.rk-discover-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(212px,1fr));gap:22px}
|
|
252
352
|
@media(max-width:520px){.rk-discover-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}}
|
|
253
|
-
.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
|
-
|
|
262
|
-
|
|
263
|
-
.rk-discover-
|
|
264
|
-
.rk-discover-
|
|
265
|
-
|
|
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}
|
|
354
|
+
.rk-discover-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:var(--rk-n-100);color:inherit}
|
|
355
|
+
.rk-discover-poster img,.rk-discover-poster video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
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);
|
|
357
|
+
background:repeating-linear-gradient(135deg,var(--rk-n-100),var(--rk-n-100) 12px,var(--rk-n-50) 12px,var(--rk-n-50) 24px)}
|
|
358
|
+
.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;
|
|
359
|
+
background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);padding:4px 9px;border-radius:var(--rk-r-full)}
|
|
360
|
+
.rk-discover-scrim{position:absolute;inset:0;z-index:1;background:linear-gradient(to top,rgba(0,0,0,.28) 0%,transparent 42%)}
|
|
361
|
+
/* wrap = positioning context so the bookmark button can sit NEXT TO (not
|
|
362
|
+
inside) the poster anchor — a button inside an <a> is invalid HTML */
|
|
363
|
+
.rk-discover-posterwrap{position:relative}
|
|
364
|
+
.rk-discover-bm{position:absolute;right:10px;bottom:10px;z-index:4;width:34px;height:34px;border:0;border-radius:var(--rk-r-full);
|
|
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;
|
|
366
|
+
transition:transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
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}}
|
|
371
|
+
.rk-discover-bm:hover{transform:scale(1.08);background:rgba(0,0,0,.6)}
|
|
372
|
+
.rk-discover-bm.is-on{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
373
|
+
.rk-discover-bm.is-on svg{fill:currentColor}
|
|
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}
|
|
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;
|
|
377
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
378
|
+
.rk-discover-desc{font-size:12.5px;font-weight:500;color:var(--rk-text-muted);line-height:1.4;
|
|
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}
|
|
266
390
|
.rk-discover-fork{gap:7px}
|
|
267
391
|
.rk-discover-fork svg{margin-top:-1px}
|
|
392
|
+
.rk-discover-fork-disabled{opacity:.6;cursor:default;pointer-events:none}
|
|
393
|
+
.rk-discover-badges{display:flex;flex-wrap:wrap;gap:6px}
|
|
394
|
+
|
|
395
|
+
/* hover-to-unmute speaker badge (video posters only): hidden until hover,
|
|
396
|
+
flips to a gold "sound on" chip once the browser actually allows audio */
|
|
397
|
+
.rk-discover-sound{position:absolute;left:12px;bottom:12px;z-index:3;width:32px;height:32px;display:none;place-items:center;color:#fff;
|
|
398
|
+
background:rgba(0,0,0,.42);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border-radius:var(--rk-r-full);pointer-events:none}
|
|
399
|
+
.rk-discover-sound span{display:grid;place-items:center;line-height:0}
|
|
400
|
+
.rk-discover-posterwrap:hover .rk-discover-sound{display:grid}
|
|
401
|
+
.rk-discover-sound .rk-ds-on{display:none}
|
|
402
|
+
.rk-discover-posterwrap.is-sound .rk-discover-sound{display:grid;background:var(--rk-gold-500);color:var(--rk-ink)}
|
|
403
|
+
.rk-discover-posterwrap.is-sound .rk-ds-on{display:grid}
|
|
404
|
+
.rk-discover-posterwrap.is-sound .rk-ds-off{display:none}
|
|
405
|
+
|
|
406
|
+
/* bookmarked-only toolbar chip (feed) — count span collapses when empty */
|
|
407
|
+
.rk-discover-bmfilter{flex:none;display:inline-flex;align-items:center;gap:7px;padding:10px 15px;border-radius:var(--rk-r-full);
|
|
408
|
+
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;
|
|
409
|
+
cursor:pointer;box-shadow:var(--rk-shadow-xs);
|
|
410
|
+
transition:color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
411
|
+
.rk-discover-bmfilter:hover{color:var(--rk-ink);border-color:var(--rk-gold-600)}
|
|
412
|
+
.rk-discover-bmfilter.is-active{background:var(--rk-gold-500);border-color:transparent;color:var(--rk-ink)}
|
|
413
|
+
.rk-discover-bmfilter.is-active svg{fill:currentColor}
|
|
414
|
+
.rk-discover-bmfilter-count:empty{display:none}
|
|
415
|
+
|
|
416
|
+
/* load / empty / error state (feed + categories) */
|
|
417
|
+
.rk-discover-state{padding:48px 16px;text-align:center;color:var(--rk-text-muted);font-size:14px;
|
|
418
|
+
background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-xs)}
|
|
419
|
+
.rk-discover-state[hidden]{display:none}
|
|
420
|
+
.rk-discover-state.is-error{color:#b91c1c;background:var(--rk-red-tint);border-color:#fecaca;font-weight:500}
|
|
421
|
+
.rk-discover-state .rk-btn{margin-top:14px}
|
|
422
|
+
|
|
423
|
+
/* ── feed: TikTok-style fullscreen viewer (click a card → vertical deck) ── */
|
|
424
|
+
html.rk-noscroll,html.rk-noscroll body{overflow:hidden}
|
|
425
|
+
.rk-discover-viewer{position:fixed;inset:0;z-index:120}
|
|
426
|
+
.rk-discover-viewer[hidden]{display:none}
|
|
427
|
+
.rk-discover-viewer-scrim{position:absolute;inset:0;background:rgba(15,12,5,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
|
428
|
+
.rk-discover-viewer-track{position:absolute;inset:0;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none}
|
|
429
|
+
.rk-discover-viewer-track::-webkit-scrollbar{display:none}
|
|
430
|
+
.rk-discover-viewer-slide{height:100%;display:grid;place-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:20px 0}
|
|
431
|
+
.rk-discover-viewer-stage{position:relative;width:min(92vw,calc(93vh * 9 / 16));aspect-ratio:9/16;max-height:100%;margin:0;
|
|
432
|
+
border-radius:var(--rk-r-3xl);overflow:hidden;background:#000;box-shadow:var(--rk-shadow-xl)}
|
|
433
|
+
.rk-discover-viewer-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
|
|
434
|
+
video.rk-discover-viewer-media{cursor:pointer}
|
|
435
|
+
.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;
|
|
436
|
+
filter:drop-shadow(0 4px 14px rgba(0,0,0,.55))}
|
|
437
|
+
.rk-discover-viewer-slide.is-paused .rk-discover-viewer-pause{display:grid}
|
|
438
|
+
.rk-discover-viewer-shade{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:1;pointer-events:none;
|
|
439
|
+
background:linear-gradient(to top,rgba(0,0,0,.74) 0%,rgba(0,0,0,.22) 62%,transparent 100%)}
|
|
440
|
+
/* right padding reserves the bookmark rail so text never runs under it */
|
|
441
|
+
.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}
|
|
442
|
+
.rk-discover-viewer-bm{position:absolute;right:14px;bottom:22px;z-index:4;width:48px;height:48px;border-radius:var(--rk-r-full);
|
|
443
|
+
border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.14);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
|
|
444
|
+
color:#fff;display:grid;place-items:center;cursor:pointer;
|
|
445
|
+
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
446
|
+
.rk-discover-viewer-bm:hover{background:rgba(255,255,255,.26);transform:scale(1.06)}
|
|
447
|
+
.rk-discover-viewer-bm.is-on{background:var(--rk-gold-500);border-color:transparent;color:var(--rk-ink)}
|
|
448
|
+
.rk-discover-viewer-bm.is-on svg{fill:currentColor}
|
|
449
|
+
.rk-discover-viewer-meta{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:rgba(255,255,255,.72)}
|
|
450
|
+
.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}
|
|
451
|
+
.rk-discover-viewer-desc{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.82);line-height:1.4;
|
|
452
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
453
|
+
.rk-discover-viewer-fork{pointer-events:auto;gap:7px;margin-top:5px}
|
|
454
|
+
.rk-discover-viewer-ui{position:absolute;top:max(16px,env(safe-area-inset-top,0px));right:16px;z-index:5;display:flex;gap:10px}
|
|
455
|
+
.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);
|
|
456
|
+
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;display:grid;place-items:center;cursor:pointer;
|
|
457
|
+
transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
458
|
+
.rk-discover-viewer-btn:hover{background:rgba(255,255,255,.24);transform:scale(1.06)}
|
|
459
|
+
.rk-discover-viewer-btn span{display:grid;place-items:center;line-height:0}
|
|
460
|
+
.rk-discover-viewer .rk-dvm-off{display:none}
|
|
461
|
+
.rk-discover-viewer.is-muted .rk-dvm-on{display:none}
|
|
462
|
+
.rk-discover-viewer.is-muted .rk-dvm-off{display:grid}
|
|
463
|
+
.rk-discover-viewer-nav{position:absolute;right:18px;top:50%;transform:translateY(-50%);z-index:5;display:grid;gap:12px}
|
|
464
|
+
@media(hover:none){.rk-discover-viewer-nav{display:none}}
|
|
465
|
+
@media(max-width:640px){
|
|
466
|
+
.rk-discover-viewer-nav{display:none}
|
|
467
|
+
.rk-discover-viewer-slide{padding:0}
|
|
468
|
+
.rk-discover-viewer-stage{width:100%;height:100%;max-height:none;aspect-ratio:auto;border-radius:0;box-shadow:none}
|
|
469
|
+
}
|
|
268
470
|
|
|
269
471
|
/* ── swipe: single-card deck ── */
|
|
270
472
|
.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
|
|
473
|
+
.rk-discover-deck{position:relative;width:min(322px,84vw);aspect-ratio:9/16;margin:6px 0}
|
|
474
|
+
.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;
|
|
475
|
+
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}
|
|
476
|
+
.rk-discover-swipe-state[hidden]{display:none}
|
|
477
|
+
.rk-discover-swipe-state.is-error{color:#b91c1c}
|
|
478
|
+
.rk-discover-swipe-state-title{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:700;color:var(--rk-ink)}
|
|
479
|
+
.rk-discover-swipe-state .rk-btn{margin-top:6px}
|
|
480
|
+
.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}
|
|
481
|
+
@keyframes rk-discover-spin{to{transform:rotate(360deg)}}
|
|
482
|
+
.rk-discover-swipe{position:absolute;inset:0;border-radius:var(--rk-r-3xl);overflow:hidden;border:1px solid var(--rk-border);background:#0b0b0b;
|
|
275
483
|
box-shadow:var(--rk-shadow-xl);transform-origin:center bottom;will-change:transform;
|
|
276
484
|
transition:transform .38s var(--rk-ease),opacity .38s var(--rk-ease)}
|
|
277
|
-
.rk-discover-swipe-
|
|
278
|
-
.rk-discover-swipe
|
|
279
|
-
|
|
280
|
-
.rk-
|
|
281
|
-
.rk-discover-swipe
|
|
282
|
-
.rk-discover-swipe-
|
|
283
|
-
.rk-discover-swipe-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.
|
|
284
|
-
.rk-discover-swipe-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
.rk-discover-
|
|
485
|
+
.rk-discover-swipe-frame{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;pointer-events:none;background:#000}
|
|
486
|
+
.rk-discover-swipe-fallback{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;
|
|
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)}
|
|
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%)}
|
|
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}
|
|
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}
|
|
492
|
+
.rk-discover-swipe-caption{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.84);line-height:1.35;
|
|
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)}
|
|
503
|
+
|
|
504
|
+
/* deck flanked by skip/fork — buttons ride the card's vertical center, 1fr gutters absorb spare width.
|
|
505
|
+
Explicit column placement keeps the deck centered when the buttons are display:none (empty/caught-up states). */
|
|
506
|
+
.rk-discover-stage{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:20px;width:100%}
|
|
507
|
+
.rk-discover-deck{grid-column:2}
|
|
293
508
|
.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)}
|
|
509
|
+
display:grid;place-items:center;cursor:pointer;box-shadow:var(--rk-shadow-md);
|
|
510
|
+
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)}
|
|
511
|
+
.rk-discover-act[hidden]{display:none}
|
|
512
|
+
.rk-discover-act-skip{grid-column:1;justify-self:end}
|
|
513
|
+
.rk-discover-act-fork{grid-column:3;justify-self:start}
|
|
514
|
+
/* no side room on small phones — card spans, buttons drop under it as a pair */
|
|
515
|
+
@media(max-width:560px){
|
|
516
|
+
.rk-discover-stage{grid-template-columns:auto auto;grid-template-areas:"deck deck" "skip fork";justify-content:center;column-gap:26px;row-gap:16px}
|
|
517
|
+
.rk-discover-deck{grid-area:deck}
|
|
518
|
+
.rk-discover-act-skip{grid-area:skip;justify-self:center}
|
|
519
|
+
.rk-discover-act-fork{grid-area:fork;justify-self:center}
|
|
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
|
+
}
|
|
296
532
|
.rk-discover-act:hover{transform:translateY(-2px);box-shadow:var(--rk-shadow-lg)}
|
|
533
|
+
.rk-discover-act:disabled{opacity:.5;cursor:default;transform:none}
|
|
297
534
|
.rk-discover-act-skip{color:var(--rk-n-500)}
|
|
298
535
|
.rk-discover-act-skip:hover{color:var(--rk-red);border-color:var(--rk-red-tint)}
|
|
299
536
|
.rk-discover-act-fork{background:var(--rk-gold-500);color:var(--rk-ink);border-color:transparent}
|
|
300
537
|
.rk-discover-act-fork:hover{background:var(--rk-gold-700);box-shadow:0 8px 22px -6px rgba(252,185,0,.6)}
|
|
301
538
|
.rk-discover-act-hint{font-size:12.5px;font-weight:600;color:var(--rk-text-faint);text-align:center;letter-spacing:.01em}
|
|
539
|
+
.rk-discover-act-hint[hidden]{display:none}
|
|
302
540
|
|
|
303
541
|
/* ── categories: browse-by-format cards ── */
|
|
304
542
|
.rk-discover-cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:20px}
|
|
305
543
|
@media(max-width:560px){.rk-discover-cats{grid-template-columns:1fr}}
|
|
544
|
+
.rk-discover-cats[hidden]{display:none}
|
|
306
545
|
.rk-discover-cat{display:flex;flex-direction:column;gap:14px;background:var(--rk-surface);border:1px solid var(--rk-border);
|
|
307
546
|
border-radius:var(--rk-r-3xl);padding:22px;box-shadow:var(--rk-shadow-card);color:inherit}
|
|
308
547
|
.rk-discover-cat-head{display:flex;align-items:center;gap:14px}
|
|
@@ -311,70 +550,1002 @@ export function renderReskinDiscover(mode = "feed") {
|
|
|
311
550
|
.rk-discover-cat-count{font-size:12.5px;font-weight:600;color:var(--rk-text-muted)}
|
|
312
551
|
.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
552
|
.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
|
|
553
|
+
.rk-discover-cat-blurb{font-size:13px;color:var(--rk-text-muted);line-height:1.45;
|
|
554
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
315
555
|
.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);
|
|
556
|
+
.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)}
|
|
557
|
+
.rk-discover-mini img,.rk-discover-mini video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
|
558
|
+
.rk-discover-mini-ph{background:linear-gradient(135deg,var(--rk-n-100),var(--rk-n-200))}
|
|
559
|
+
|
|
560
|
+
.rk-discover-cat-reveal[hidden]{display:none}
|
|
561
|
+
/* reveal header shares the toolbar row with the mode toggle (user feedback) */
|
|
562
|
+
.rk-discover-reveal-head{display:flex;align-items:center;gap:12px;min-width:0}
|
|
563
|
+
.rk-discover-reveal-head[hidden]{display:none}
|
|
564
|
+
.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;
|
|
565
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
566
|
+
.rk-discover-cats-state[hidden]{display:none}
|
|
317
567
|
`;
|
|
318
568
|
const script = `
|
|
319
569
|
(function(){
|
|
320
570
|
var root=document;
|
|
321
|
-
|
|
322
|
-
var
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
571
|
+
var bootEl=root.querySelector('[data-rk-discover-boot]');
|
|
572
|
+
var boot={};
|
|
573
|
+
try{ boot=JSON.parse(bootEl&&bootEl.textContent?bootEl.textContent:'{}')||{}; }catch(e){ boot={}; }
|
|
574
|
+
var feedEndpoint=boot.feedEndpoint||'/discover/feed';
|
|
575
|
+
var popularEndpoint=boot.popularEndpoint||'/discover/popular';
|
|
576
|
+
var swipeStackEndpoint=boot.swipeStackEndpoint||'/discover/swipe/stack';
|
|
577
|
+
var swipeRefillEndpoint=boot.swipeRefillEndpoint||'/discover/swipe/refill';
|
|
578
|
+
var swipeBase=boot.swipeBase||'/discover/swipe';
|
|
579
|
+
var editorBase=boot.editorBase||'/editor';
|
|
580
|
+
var accountId=boot.accountId||'';
|
|
581
|
+
|
|
582
|
+
function esc(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
583
|
+
function attr(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
|
584
|
+
function isVideo(u){ return Boolean(u)&&/\\.(mp4|webm|ogg|mov)(\\?|#|$)/i.test(u); }
|
|
585
|
+
// previewUrl falls back to the template's raw videoUrl, which is often a
|
|
586
|
+
// signed/extension-less URL — so treat "not a known image" as video, never
|
|
587
|
+
// the other way round, or those templates render a broken <img> (no preview).
|
|
588
|
+
function isImage(u){ return Boolean(u)&&/\\.(png|jpe?g|gif|webp|avif|svg)(\\?|#|$)/i.test(u); }
|
|
589
|
+
function withAccount(u){
|
|
590
|
+
try{ var url=new URL(u, window.location.href); if(accountId) url.searchParams.set('account', accountId); return url.toString(); }
|
|
591
|
+
catch(e){ return u; }
|
|
592
|
+
}
|
|
593
|
+
function editorUrl(id){
|
|
594
|
+
var u=editorBase+'/'+encodeURIComponent(String(id));
|
|
595
|
+
return accountId? u+'?account='+encodeURIComponent(accountId) : u;
|
|
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
|
+
});
|
|
652
|
+
function fmtDuration(s){
|
|
653
|
+
s=Number(s); if(!s||!isFinite(s)||s<=0) return '';
|
|
654
|
+
if(s>=60){ var m=Math.floor(s/60); var r=Math.round(s%60); return m+':'+(r<10?'0':'')+r; }
|
|
655
|
+
return (s>=10? s.toFixed(0) : s.toFixed(1))+'s';
|
|
656
|
+
}
|
|
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'};
|
|
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); }
|
|
659
|
+
var EDIT_ICON=${JSON.stringify(EDIT_ICON)};
|
|
660
|
+
var COPY_ICON=${JSON.stringify(COPY_ICON)};
|
|
661
|
+
var IDCHECK_ICON=${JSON.stringify(IDCHECK_ICON)};
|
|
662
|
+
var BOOKMARK_ICON=${JSON.stringify(BOOKMARK_ICON)};
|
|
663
|
+
var SOUND_ON_ICON=${JSON.stringify(SOUND_ON_ICON)};
|
|
664
|
+
var SOUND_OFF_ICON=${JSON.stringify(SOUND_OFF_ICON)};
|
|
665
|
+
var PLAY_ICON=${JSON.stringify(PLAY_ICON)};
|
|
666
|
+
|
|
667
|
+
// ── bookmarks: client-side saves in localStorage (no backend favorite
|
|
668
|
+
// endpoint exists), shared by feed cards, category reveal cards and the
|
|
669
|
+
// viewer slides. One delegated document listener toggles any [data-bm].
|
|
670
|
+
var BM_KEY='rk-discover-bookmarks';
|
|
671
|
+
var bookmarks=(function(){
|
|
672
|
+
try{ var v=JSON.parse(window.localStorage.getItem(BM_KEY)||'[]'); return new Set(Array.isArray(v)?v.map(String):[]); }
|
|
673
|
+
catch(e){ return new Set(); }
|
|
674
|
+
})();
|
|
675
|
+
var bmChangeHandlers=[];
|
|
676
|
+
function isBookmarked(id){ return Boolean(id)&&bookmarks.has(String(id)); }
|
|
677
|
+
function syncBmButtons(){
|
|
678
|
+
root.querySelectorAll('[data-bm]').forEach(function(b){
|
|
679
|
+
var on=isBookmarked(b.getAttribute('data-bm'));
|
|
680
|
+
b.classList.toggle('is-on',on);
|
|
681
|
+
b.setAttribute('aria-pressed',on?'true':'false');
|
|
682
|
+
b.setAttribute('aria-label',on?'Remove bookmark':'Bookmark');
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
function toggleBookmark(id){
|
|
686
|
+
id=String(id||''); if(!id) return;
|
|
687
|
+
if(bookmarks.has(id)) bookmarks.delete(id); else bookmarks.add(id);
|
|
688
|
+
try{ window.localStorage.setItem(BM_KEY, JSON.stringify(Array.from(bookmarks))); }catch(e){}
|
|
689
|
+
syncBmButtons();
|
|
690
|
+
bmChangeHandlers.forEach(function(fn){ try{ fn(id); }catch(e){} });
|
|
691
|
+
}
|
|
692
|
+
function bmBtnHtml(id, cls){
|
|
693
|
+
if(!id) return '';
|
|
694
|
+
var on=isBookmarked(id);
|
|
695
|
+
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>';
|
|
696
|
+
}
|
|
697
|
+
document.addEventListener('click',function(e){
|
|
698
|
+
var b=(e.target&&e.target.closest)?e.target.closest('[data-bm]'):null;
|
|
699
|
+
if(!b) return;
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
toggleBookmark(b.getAttribute('data-bm'));
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// ── shared: pause/resume a grid's muted autoplay (while the viewer is open) ──
|
|
705
|
+
function parkVideos(container){
|
|
706
|
+
if(!container) return;
|
|
707
|
+
container.querySelectorAll('video').forEach(function(o){ o.muted=true; o.pause(); });
|
|
708
|
+
container.querySelectorAll('.rk-discover-posterwrap.is-sound').forEach(function(p){ p.classList.remove('is-sound'); });
|
|
709
|
+
}
|
|
710
|
+
function resumeVideos(container){
|
|
711
|
+
if(!container) return;
|
|
712
|
+
container.querySelectorAll('video').forEach(function(o){ o.muted=true; var pr=o.play(); if(pr&&pr.catch) pr.catch(function(){}); });
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function friendlyError(err){
|
|
716
|
+
var msg=(err&&err.message)?String(err.message):'';
|
|
717
|
+
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.";
|
|
718
|
+
return msg;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function mediaHtml(t){
|
|
722
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
723
|
+
if(!url) return '<div class="rk-discover-media-empty">'+(t.status==='processing'?'Downloading…':'No preview')+'</div>';
|
|
724
|
+
if(isImage(url)) return '<img src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'" loading="lazy" decoding="async">';
|
|
725
|
+
// #t=0.1 paints the first frame even where autoplay is blocked.
|
|
726
|
+
return '<video src="'+attr(url+'#t=0.1')+'" muted loop autoplay playsinline preload="metadata" tabindex="-1"></video>';
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Real feed/category poster card. The "Open Editor" CTA always deep-links
|
|
730
|
+
// into the real editor; the poster itself carries data-rk-view=<index> so a
|
|
731
|
+
// plain click opens the TikTok-style viewer instead (cmd/middle-click still
|
|
732
|
+
// opens the editor via the untouched href). No platform/source pill on the
|
|
733
|
+
// poster (user feedback) — source shows only in the viewer's meta line.
|
|
734
|
+
function posterCardHtml(t, viewIdx){
|
|
735
|
+
var id=domId(t);
|
|
736
|
+
var tid=realTemplateId(t);
|
|
737
|
+
var remix=needsDecompose(t);
|
|
738
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
739
|
+
var hasVideo=Boolean(url)&&!isImage(url);
|
|
740
|
+
var pending=(t.status==='processing'||t.status==='failed');
|
|
741
|
+
var viewable=!pending&&Boolean(url)&&typeof viewIdx==='number'&&viewIdx>=0;
|
|
742
|
+
var dur=fmtDuration(t.durationSeconds);
|
|
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);
|
|
748
|
+
var posterInner=mediaHtml(t)
|
|
749
|
+
+(dur?'<span class="rk-discover-dur">'+esc(dur)+'</span>':'')
|
|
750
|
+
+'<div class="rk-discover-scrim"></div>'
|
|
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>':'');
|
|
752
|
+
var poster=pending
|
|
753
|
+
? '<div class="rk-discover-poster">'+posterInner+'</div>'
|
|
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>';
|
|
755
|
+
// bookmark sits NEXT TO the anchor inside the wrap (button-in-anchor is
|
|
756
|
+
// invalid HTML); the wrap is the shared hover context for sound + bookmark
|
|
757
|
+
poster='<div class="rk-discover-posterwrap">'+poster+(pending?'':bmBtnHtml(id,'rk-discover-bm'))+'</div>';
|
|
758
|
+
var cta=pending
|
|
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>'
|
|
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>';
|
|
763
|
+
return '<article class="rk-discover-card rk-card-hover">'
|
|
764
|
+
+ poster
|
|
765
|
+
+ '<div class="rk-discover-body">'
|
|
766
|
+
+ '<div class="rk-discover-titlewrap">'
|
|
767
|
+
+ '<h3 class="rk-discover-title">'+esc(t.title||id)+'</h3>'
|
|
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>':'')
|
|
770
|
+
+ '</div>'
|
|
771
|
+
+ cta
|
|
772
|
+
+ '</div>'
|
|
773
|
+
+ '</article>';
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ── hover audio: unmute a poster's (muted, autoplaying) video while the
|
|
777
|
+
// pointer is over it; only one card is ever audible at a time. Browsers may
|
|
778
|
+
// refuse unmuted playback before the user's first click (autoplay policy) —
|
|
779
|
+
// in that case we verify shortly after and fall back to muted playback
|
|
780
|
+
// instead of leaving a frozen poster. Delegated, so grid re-renders keep it.
|
|
781
|
+
function bindHoverAudio(container){
|
|
782
|
+
if(!container) return;
|
|
783
|
+
function posterOf(e){ var t=e.target; return (t&&t.closest)?t.closest('.rk-discover-posterwrap'):null; }
|
|
784
|
+
function setSound(poster,on){
|
|
785
|
+
var v=poster.querySelector('video'); if(!v) return;
|
|
786
|
+
if(on){
|
|
787
|
+
container.querySelectorAll('video').forEach(function(o){ if(o!==v) o.muted=true; });
|
|
788
|
+
container.querySelectorAll('.rk-discover-posterwrap.is-sound').forEach(function(p){ if(p!==poster) p.classList.remove('is-sound'); });
|
|
789
|
+
v.muted=false;
|
|
790
|
+
if(v.paused){ var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){}); }
|
|
791
|
+
window.setTimeout(function(){
|
|
792
|
+
if(v.muted) return; // already reverted (pointer left)
|
|
793
|
+
if(v.paused){ v.muted=true; poster.classList.remove('is-sound'); var pr2=v.play(); if(pr2&&pr2.catch) pr2.catch(function(){}); }
|
|
794
|
+
else poster.classList.add('is-sound');
|
|
795
|
+
},70);
|
|
796
|
+
} else {
|
|
797
|
+
v.muted=true; poster.classList.remove('is-sound');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
container.addEventListener('mouseover',function(e){
|
|
801
|
+
var p=posterOf(e); if(!p) return;
|
|
802
|
+
if(e.relatedTarget&&p.contains(e.relatedTarget)) return; // still inside the same poster
|
|
803
|
+
setSound(p,true);
|
|
804
|
+
});
|
|
805
|
+
container.addEventListener('mouseout',function(e){
|
|
806
|
+
var p=posterOf(e); if(!p) return;
|
|
807
|
+
if(e.relatedTarget&&p.contains(e.relatedTarget)) return;
|
|
808
|
+
setSound(p,false);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ── TikTok-style viewer modal — SHARED by the feed grid and the categories
|
|
813
|
+
// reveal grid. open(list, clickedIdx, park): list is the template array
|
|
814
|
+
// behind the grid the user clicked, clickedIdx the clicked card index in
|
|
815
|
+
// that list, park the grid container whose muted autoplay is paused while
|
|
816
|
+
// the modal owns audio + CPU. Slides hydrate lazily (active +/-1); the click
|
|
817
|
+
// that opens the modal is a real user gesture, so the active video plays
|
|
818
|
+
// WITH sound (with a muted-playback fallback if the browser still refuses).
|
|
819
|
+
var viewerCtl=(function(){
|
|
820
|
+
var viewer=root.querySelector('[data-rk-viewer]');
|
|
821
|
+
if(!viewer) return null;
|
|
822
|
+
var vTrack=viewer.querySelector('[data-viewer-track]');
|
|
823
|
+
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null; var parkEl=null;
|
|
824
|
+
|
|
825
|
+
function viewerSlideHtml(t,i){
|
|
826
|
+
var id=domId(t);
|
|
827
|
+
var tid=realTemplateId(t);
|
|
828
|
+
var remix=needsDecompose(t);
|
|
829
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
830
|
+
var media=isImage(url)
|
|
831
|
+
? '<img class="rk-discover-viewer-media" src="'+attr(url)+'" alt="'+attr((t.title||'Template')+' preview')+'">'
|
|
832
|
+
: '<video class="rk-discover-viewer-media" data-vsrc="'+attr(url)+'" loop playsinline preload="none"></video>';
|
|
833
|
+
var meta=[];
|
|
834
|
+
var srcL=sourceLabel(t.sourceType); if(srcL) meta.push(esc(srcL));
|
|
835
|
+
var dur=fmtDuration(t.durationSeconds); if(dur) meta.push(esc(dur));
|
|
836
|
+
var desc=String(t.viralDna||t.summary||'').trim();
|
|
837
|
+
return '<section class="rk-discover-viewer-slide" data-vslide="'+i+'">'
|
|
838
|
+
+'<figure class="rk-discover-viewer-stage">'
|
|
839
|
+
+ media
|
|
840
|
+
+'<span class="rk-discover-viewer-pause">'+PLAY_ICON+'</span>'
|
|
841
|
+
+'<div class="rk-discover-viewer-shade"></div>'
|
|
842
|
+
+ bmBtnHtml(id,'rk-discover-viewer-bm')
|
|
843
|
+
+'<div class="rk-discover-viewer-info">'
|
|
844
|
+
+(meta.length?'<div class="rk-discover-viewer-meta">'+meta.join(' · ')+'</div>':'')
|
|
845
|
+
+'<div class="rk-discover-viewer-title">'+esc(t.title||id)+'</div>'
|
|
846
|
+
+(desc?'<div class="rk-discover-viewer-desc">'+esc(desc)+'</div>':'')
|
|
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>')
|
|
850
|
+
+'</div>'
|
|
851
|
+
+'</figure>'
|
|
852
|
+
+'</section>';
|
|
853
|
+
}
|
|
854
|
+
function hydrateSlide(slide){
|
|
855
|
+
var v=slide.querySelector('video[data-vsrc]');
|
|
856
|
+
if(!v||v.getAttribute('src')) return;
|
|
857
|
+
v.preload='auto';
|
|
858
|
+
v.setAttribute('src', v.getAttribute('data-vsrc'));
|
|
859
|
+
}
|
|
860
|
+
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
861
|
+
function setViewerActive(idx){
|
|
862
|
+
vActive=idx;
|
|
863
|
+
[].forEach.call(vTrack.children,function(s,i){
|
|
864
|
+
if(Math.abs(i-idx)<=1) hydrateSlide(s);
|
|
865
|
+
s.classList.remove('is-paused');
|
|
866
|
+
var v=s.querySelector('video');
|
|
867
|
+
if(!v) return;
|
|
868
|
+
if(i===idx){
|
|
869
|
+
v.muted=!vSound;
|
|
870
|
+
var pr=v.play();
|
|
871
|
+
if(pr&&pr.catch) pr.catch(function(){
|
|
872
|
+
// sound-on autoplay blocked → fall back to muted playback
|
|
873
|
+
v.muted=true; vSound=false; updateMuteUi();
|
|
874
|
+
var pr2=v.play(); if(pr2&&pr2.catch) pr2.catch(function(){});
|
|
875
|
+
});
|
|
876
|
+
} else { v.pause(); }
|
|
877
|
+
});
|
|
878
|
+
updateMuteUi();
|
|
879
|
+
}
|
|
880
|
+
function stepViewer(d){
|
|
881
|
+
if(!vOpen) return;
|
|
882
|
+
var next=Math.max(0,Math.min(vItems.length-1,vActive+d));
|
|
883
|
+
if(next===vActive) return;
|
|
884
|
+
vTrack.scrollTo({top:next*vTrack.clientHeight,behavior:'smooth'});
|
|
885
|
+
}
|
|
886
|
+
function open(list, clickedIdx, park){
|
|
887
|
+
vItems=[]; var start=0;
|
|
888
|
+
(list||[]).forEach(function(t,i){
|
|
889
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
890
|
+
if(!url||t.status==='processing'||t.status==='failed') return;
|
|
891
|
+
if(i===clickedIdx) start=vItems.length;
|
|
892
|
+
vItems.push(t);
|
|
893
|
+
});
|
|
894
|
+
if(!vItems.length) return;
|
|
895
|
+
parkEl=park||null;
|
|
896
|
+
vTrack.innerHTML=vItems.map(viewerSlideHtml).join('');
|
|
897
|
+
vOpen=true; vSound=true;
|
|
898
|
+
viewer.hidden=false;
|
|
899
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
900
|
+
parkVideos(parkEl);
|
|
901
|
+
if(vIO) vIO.disconnect();
|
|
902
|
+
vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){
|
|
903
|
+
entries.forEach(function(en){
|
|
904
|
+
if(!en.isIntersecting) return;
|
|
905
|
+
var i=Number(en.target.getAttribute('data-vslide'));
|
|
906
|
+
if(i!==vActive) setViewerActive(i);
|
|
907
|
+
});
|
|
908
|
+
},{root:vTrack,threshold:0.6}):null;
|
|
909
|
+
if(vIO) [].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
910
|
+
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
911
|
+
setViewerActive(start);
|
|
912
|
+
}
|
|
913
|
+
function close(){
|
|
914
|
+
if(!vOpen) return;
|
|
915
|
+
vOpen=false; vActive=-1;
|
|
916
|
+
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
917
|
+
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
918
|
+
vTrack.innerHTML='';
|
|
919
|
+
viewer.hidden=true;
|
|
920
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
921
|
+
resumeVideos(parkEl);
|
|
922
|
+
parkEl=null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
926
|
+
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
927
|
+
if(muteBtn) muteBtn.addEventListener('click',function(){
|
|
928
|
+
vSound=!vSound; updateMuteUi();
|
|
929
|
+
var s=vTrack.children[vActive]; var v=s&&s.querySelector('video');
|
|
930
|
+
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'); } }
|
|
931
|
+
});
|
|
932
|
+
var prevBtn=viewer.querySelector('[data-viewer-prev]');
|
|
933
|
+
var nextBtn=viewer.querySelector('[data-viewer-next]');
|
|
934
|
+
if(prevBtn) prevBtn.addEventListener('click',function(){ stepViewer(-1); });
|
|
935
|
+
if(nextBtn) nextBtn.addEventListener('click',function(){ stepViewer(1); });
|
|
936
|
+
// tap the video → pause/resume (TikTok); tap the empty gutter → close;
|
|
937
|
+
// bookmarks are handled by the document-level [data-bm] listener
|
|
938
|
+
vTrack.addEventListener('click',function(e){
|
|
939
|
+
var t=e.target;
|
|
940
|
+
if(t&&t.closest&&(t.closest('a')||t.closest('[data-bm]'))) return;
|
|
941
|
+
var media=(t&&t.closest)?t.closest('.rk-discover-viewer-media'):null;
|
|
942
|
+
if(media&&media.tagName==='VIDEO'){
|
|
943
|
+
var slide=media.closest('.rk-discover-viewer-slide');
|
|
944
|
+
if(media.paused){ var pr=media.play(); if(pr&&pr.catch) pr.catch(function(){}); if(slide) slide.classList.remove('is-paused'); }
|
|
945
|
+
else { media.pause(); if(slide) slide.classList.add('is-paused'); }
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if(t&&t.classList&&(t.classList.contains('rk-discover-viewer-slide')||t===vTrack)) close();
|
|
949
|
+
});
|
|
950
|
+
document.addEventListener('keydown',function(e){
|
|
951
|
+
if(!vOpen) return;
|
|
952
|
+
if(e.key==='Escape'){ e.preventDefault(); close(); }
|
|
953
|
+
else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); stepViewer(1); }
|
|
954
|
+
else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); stepViewer(-1); }
|
|
955
|
+
});
|
|
956
|
+
window.addEventListener('resize',function(){
|
|
957
|
+
if(vOpen&&vActive>=0) vTrack.scrollTop=vActive*vTrack.clientHeight;
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
return { open: open, close: close, isOpen: function(){ return vOpen; } };
|
|
961
|
+
})();
|
|
962
|
+
|
|
963
|
+
// Wire a poster grid's plain-clicks into the shared viewer. getList is
|
|
964
|
+
// called at click time so it always reflects the grid's CURRENT contents
|
|
965
|
+
// (search filter, bookmark filter, revealed group).
|
|
966
|
+
function bindViewerOpens(container, getList){
|
|
967
|
+
if(!container||!viewerCtl) return;
|
|
968
|
+
container.addEventListener('click',function(e){
|
|
969
|
+
if(e.defaultPrevented||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0) return;
|
|
970
|
+
var a=(e.target&&e.target.closest)?e.target.closest('[data-rk-view]'):null;
|
|
971
|
+
if(!a||!container.contains(a)) return;
|
|
972
|
+
e.preventDefault();
|
|
973
|
+
viewerCtl.open(getList()||[], Number(a.getAttribute('data-rk-view')), container);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
978
|
+
// FEED
|
|
979
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
980
|
+
var feedRoot=root.querySelector('[data-rk-feed]');
|
|
981
|
+
if(feedRoot){
|
|
982
|
+
var gridEl=feedRoot.querySelector('[data-rk-grid]');
|
|
983
|
+
var stateEl=feedRoot.querySelector('[data-rk-state]');
|
|
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.
|
|
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
|
+
}
|
|
996
|
+
var bmChip=root.querySelector('[data-rk-bmfilter]');
|
|
997
|
+
var bmCountEl=root.querySelector('[data-rk-bmcount]');
|
|
998
|
+
var bmFilter=false;
|
|
999
|
+
var feedState={items:[],visible:[],loading:false};
|
|
1000
|
+
var feedReq=0;
|
|
1001
|
+
|
|
1002
|
+
function feedSetState(msg,isError){
|
|
1003
|
+
if(gridEl) gridEl.innerHTML='';
|
|
1004
|
+
if(stateEl){ stateEl.hidden=false; stateEl.innerHTML=msg; stateEl.classList.toggle('is-error',!!isError); }
|
|
1005
|
+
setCountHint(null);
|
|
1006
|
+
}
|
|
1007
|
+
function updateBmChip(){
|
|
1008
|
+
if(!bmChip) return;
|
|
1009
|
+
var n=0;
|
|
1010
|
+
feedState.items.forEach(function(t){ if(isBookmarked(t.templateId||t.template_id)) n++; });
|
|
1011
|
+
if(bmCountEl) bmCountEl.textContent=n?String(n):'';
|
|
1012
|
+
bmChip.classList.toggle('is-active',bmFilter);
|
|
1013
|
+
bmChip.setAttribute('aria-pressed',bmFilter?'true':'false');
|
|
1014
|
+
}
|
|
1015
|
+
function feedRender(){
|
|
1016
|
+
var vis=bmFilter
|
|
1017
|
+
? feedState.items.filter(function(t){ return isBookmarked(t.templateId||t.template_id); })
|
|
1018
|
+
: feedState.items;
|
|
1019
|
+
feedState.visible=vis;
|
|
1020
|
+
updateBmChip();
|
|
1021
|
+
if(!vis.length){
|
|
1022
|
+
feedSetState(bmFilter?'No bookmarks yet — tap the bookmark on any template to save it for later.':'No templates match — try a different search.',false);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if(stateEl) stateEl.hidden=true;
|
|
1026
|
+
if(gridEl) gridEl.innerHTML=vis.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
1027
|
+
setCountHint(vis.length);
|
|
1028
|
+
}
|
|
1029
|
+
if(bmChip) bmChip.addEventListener('click',function(){ bmFilter=!bmFilter; feedRender(); });
|
|
1030
|
+
bmChangeHandlers.push(function(){
|
|
1031
|
+
updateBmChip();
|
|
1032
|
+
// with the filter on, un-bookmarking drops the card from the grid; the
|
|
1033
|
+
// re-render restarts muted autoplay, so re-park it if the viewer is open
|
|
1034
|
+
if(bmFilter){ feedRender(); if(viewerCtl&&viewerCtl.isOpen()) parkVideos(gridEl); }
|
|
1035
|
+
});
|
|
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){
|
|
1044
|
+
var url=feedEndpoint;
|
|
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; }
|
|
1046
|
+
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
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(); })
|
|
1048
|
+
.then(function(j){ if(my!==feedReq) return; applyFeedItems((j&&Array.isArray(j.templates))?j.templates:[]); })
|
|
1049
|
+
.catch(function(err){ if(my!==feedReq) return; feedState.loading=false; feedSetState(esc(friendlyError(err)),true); });
|
|
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
|
+
}
|
|
1072
|
+
// Honor a ?q= (e.g. from a category link) in the page URL.
|
|
1073
|
+
var initialQuery='';
|
|
1074
|
+
try{ initialQuery=(new URL(window.location.href)).searchParams.get('q')||''; }catch(e){ initialQuery=''; }
|
|
1075
|
+
if(searchEl){
|
|
1076
|
+
searchEl.value=initialQuery;
|
|
1077
|
+
var debounce=null;
|
|
1078
|
+
searchEl.addEventListener('input',function(){
|
|
1079
|
+
if(debounce) window.clearTimeout(debounce);
|
|
1080
|
+
var v=searchEl.value.trim();
|
|
1081
|
+
debounce=window.setTimeout(function(){ feedLoad(v); },260);
|
|
1082
|
+
});
|
|
1083
|
+
searchEl.addEventListener('keydown',function(e){ if(e.key==='Enter'){ if(debounce) window.clearTimeout(debounce); feedLoad(searchEl.value.trim()); } });
|
|
1084
|
+
}
|
|
1085
|
+
bindHoverAudio(gridEl);
|
|
1086
|
+
// plain click on a card → shared viewer over the CURRENT visible feed
|
|
1087
|
+
// (search + bookmark filter applied)
|
|
1088
|
+
bindViewerOpens(gridEl, function(){ return feedState.visible; });
|
|
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
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1226
|
+
// SWIPE
|
|
1227
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1228
|
+
var swipeRoot=root.querySelector('[data-rk-swipe]');
|
|
1229
|
+
if(swipeRoot){
|
|
1230
|
+
var deck=swipeRoot.querySelector('[data-deck]');
|
|
1231
|
+
var swStateEl=swipeRoot.querySelector('[data-rk-swipe-state]');
|
|
1232
|
+
var actionBtns=[].slice.call(swipeRoot.querySelectorAll('[data-swipe-act]'));
|
|
1233
|
+
var hint=swipeRoot.querySelector('[data-actions-hint]');
|
|
1234
|
+
var skipBtn=swipeRoot.querySelector('[data-swipe-skip]');
|
|
1235
|
+
var forkBtn=swipeRoot.querySelector('[data-swipe-fork]');
|
|
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:{}};
|
|
1238
|
+
var nodes=[]; // live card DOM nodes, top-of-deck first
|
|
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
|
+
|
|
1262
|
+
function swShowState(html,kind){
|
|
1263
|
+
if(swStateEl){ swStateEl.hidden=false; swStateEl.innerHTML=html; swStateEl.classList.toggle('is-error',kind==='error'); }
|
|
1264
|
+
actionBtns.forEach(function(b){ b.hidden=true; });
|
|
1265
|
+
if(hint) hint.hidden=true;
|
|
1266
|
+
}
|
|
1267
|
+
function swHideState(){ if(swStateEl) swStateEl.hidden=true; }
|
|
1268
|
+
|
|
1269
|
+
function makeCard(card){
|
|
1270
|
+
if(card&&card.id) sw.known[String(card.id)]=true;
|
|
1271
|
+
var el=document.createElement('article');
|
|
1272
|
+
el.className='rk-discover-swipe';
|
|
1273
|
+
el._card=card; el._iframed=false;
|
|
1274
|
+
var title=esc(card.title||card.source_title||card.template_id||'Template');
|
|
1275
|
+
var caption=card.caption?('<div class="rk-discover-swipe-caption">'+esc(card.caption)+'</div>'):'';
|
|
1276
|
+
el.innerHTML='<div class="rk-discover-swipe-fallback"><span>'+title+'</span></div>'
|
|
1277
|
+
+'<div class="rk-discover-swipe-scrim"></div>'
|
|
1278
|
+
+'<div class="rk-discover-swipe-info"><div class="rk-discover-swipe-title">'+title+'</div>'+caption+'</div>';
|
|
1279
|
+
deck.appendChild(el);
|
|
1280
|
+
return el;
|
|
1281
|
+
}
|
|
1282
|
+
function upgradeToIframe(el){
|
|
1283
|
+
if(el._iframed) return;
|
|
1284
|
+
var card=el._card; var url=card&&(card.preview_url||card.previewUrl);
|
|
1285
|
+
if(!url) return;
|
|
1286
|
+
var fb=el.querySelector('.rk-discover-swipe-fallback');
|
|
1287
|
+
var frame=document.createElement('iframe');
|
|
1288
|
+
frame.className='rk-discover-swipe-frame';
|
|
1289
|
+
frame.setAttribute('src', url);
|
|
1290
|
+
frame.setAttribute('title', String(card.title||'Preview'));
|
|
1291
|
+
frame.setAttribute('loading','lazy');
|
|
1292
|
+
frame.setAttribute('scrolling','no');
|
|
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);
|
|
1301
|
+
if(fb&&fb.parentNode) fb.parentNode.insertBefore(frame, fb); else el.insertBefore(frame, el.firstChild);
|
|
1302
|
+
el._iframed=true;
|
|
1303
|
+
}
|
|
329
1304
|
function restack(){
|
|
330
|
-
|
|
331
|
-
if(i>2){
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1305
|
+
nodes.forEach(function(el,i){
|
|
1306
|
+
if(i>2){ el.style.display='none'; return; }
|
|
1307
|
+
el.style.display='';
|
|
1308
|
+
el.style.zIndex=String(20-i);
|
|
1309
|
+
el.style.transform='translateY('+(i*12)+'px) scale('+(1-i*0.045).toFixed(3)+')';
|
|
1310
|
+
el.style.opacity=i===0?'1':(i===1?'0.92':'0.82');
|
|
1311
|
+
if(i<=1) upgradeToIframe(el);
|
|
337
1312
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
1313
|
+
var has=nodes.length>0;
|
|
1314
|
+
actionBtns.forEach(function(b){ b.hidden=!has; });
|
|
1315
|
+
if(hint) hint.hidden=!has;
|
|
1316
|
+
syncSound(); updateSoundBtn();
|
|
1317
|
+
if(!has) swCaughtUp();
|
|
1318
|
+
}
|
|
1319
|
+
function swCaughtUp(){
|
|
1320
|
+
if(sw.customizing>0||sw.refilling){
|
|
1321
|
+
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>');
|
|
1322
|
+
} else {
|
|
1323
|
+
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
1324
|
}
|
|
343
1325
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
1326
|
+
function decide(dir){
|
|
1327
|
+
if(sw.busy||!nodes.length) return;
|
|
1328
|
+
sw.busy=true;
|
|
1329
|
+
var el=nodes.shift();
|
|
1330
|
+
var card=el._card;
|
|
1331
|
+
el.classList.remove('is-front');
|
|
1332
|
+
el.style.zIndex='30';
|
|
1333
|
+
el.style.transform='translateX('+(dir==='fork'?460:-460)+'px) rotate('+(dir==='fork'?15:-15)+'deg)';
|
|
1334
|
+
el.style.opacity='0';
|
|
1335
|
+
window.setTimeout(function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); },380);
|
|
1336
|
+
var path=swipeBase+'/'+encodeURIComponent(String(card.id))+'/'+(dir==='fork'?'approve':'reject');
|
|
1337
|
+
fetch(withAccount(path),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
1338
|
+
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
1339
|
+
.catch(function(){ /* swallowed — the card already left the deck */ })
|
|
1340
|
+
.then(function(){ sw.busy=false; restack(); refill(); });
|
|
1341
|
+
}
|
|
1342
|
+
function refill(){
|
|
1343
|
+
if(sw.refilling||!sw.productSet||!sw.loaded) return;
|
|
1344
|
+
if(nodes.length>=sw.target) return;
|
|
1345
|
+
sw.refilling=true;
|
|
1346
|
+
fetch(withAccount(swipeRefillEndpoint),{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json','accept':'application/json'},body:'{}'})
|
|
1347
|
+
.then(function(r){ return r.json().catch(function(){ return {}; }); })
|
|
1348
|
+
.then(function(j){
|
|
1349
|
+
sw.refilling=false;
|
|
1350
|
+
if(!j||j.ok===false) return; // e.g. need_provider_key — stop quietly
|
|
1351
|
+
if(j.filled&&j.record){ makeCard(j.record); restack(); }
|
|
1352
|
+
var more=j.filled&&nodes.length<sw.target&&j.reason!=='no_eligible'&&j.reason!=='at_target';
|
|
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();
|
|
1389
|
+
})
|
|
1390
|
+
.catch(function(){ pollTries++; schedulePoll(); });
|
|
1391
|
+
}
|
|
1392
|
+
function loadStack(){
|
|
1393
|
+
swShowState('<div class="rk-discover-spinner" aria-hidden="true"></div><div>Loading your deck…</div>');
|
|
1394
|
+
fetch(withAccount(swipeStackEndpoint),{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1395
|
+
.then(function(r){
|
|
1396
|
+
if(r.status===401){ var e=new Error('unauth'); e.code=401; throw e; }
|
|
1397
|
+
if(!r.ok){ throw new Error("Couldn't load your deck ("+r.status+")."); }
|
|
1398
|
+
return r.json();
|
|
1399
|
+
})
|
|
1400
|
+
.then(function(j){
|
|
1401
|
+
sw.loaded=true;
|
|
1402
|
+
sw.target=Number(j&&j.target_depth)||5;
|
|
1403
|
+
sw.customizing=Number(j&&j.customizing)||0;
|
|
1404
|
+
sw.productSet=Boolean(j&&j.product_description_set);
|
|
1405
|
+
var stack=(j&&Array.isArray(j.stack))?j.stack:[];
|
|
1406
|
+
if(!sw.productSet){
|
|
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>');
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
nodes=stack.map(makeCard);
|
|
1411
|
+
if(nodes.length){ swHideState(); restack(); }
|
|
1412
|
+
else { swCaughtUp(); }
|
|
1413
|
+
refill();
|
|
1414
|
+
})
|
|
1415
|
+
.catch(function(err){
|
|
1416
|
+
if(err&&err.code===401){
|
|
1417
|
+
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>');
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
swShowState('<div class="rk-discover-swipe-state-title">Couldn\\u2019t load your deck</div><div>'+esc(friendlyError(err))+'</div>','error');
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
if(skipBtn) skipBtn.addEventListener('click',function(){ decide('skip'); });
|
|
1424
|
+
if(forkBtn) forkBtn.addEventListener('click',function(){ decide('fork'); });
|
|
1425
|
+
loadStack();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1429
|
+
// CATEGORIES
|
|
1430
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1431
|
+
var catsRoot=root.querySelector('[data-rk-cats]');
|
|
1432
|
+
if(catsRoot){
|
|
1433
|
+
var catsGrid=catsRoot.querySelector('[data-rk-cats-grid]');
|
|
1434
|
+
var reveal=catsRoot.querySelector('[data-rk-cat-reveal]');
|
|
1435
|
+
var revealGrid=catsRoot.querySelector('[data-rk-reveal-grid]');
|
|
1436
|
+
// reveal header ("← All categories" + group title) lives in the toolbar row
|
|
1437
|
+
var revealHead=root.querySelector('[data-rk-reveal-head]');
|
|
1438
|
+
var revealTitle=root.querySelector('[data-rk-reveal-title]');
|
|
1439
|
+
var backBtn=root.querySelector('[data-rk-cat-back]');
|
|
1440
|
+
var catsState=catsRoot.querySelector('[data-rk-cats-state]');
|
|
1441
|
+
var groups={}; // label -> [templates]
|
|
1442
|
+
var revealItems=[]; // the currently-revealed group's templates (viewer list)
|
|
1443
|
+
|
|
1444
|
+
function miniHtml(t){
|
|
1445
|
+
var url=t.previewUrl||t.preview_url||'';
|
|
1446
|
+
if(!url) return '<span class="rk-discover-mini rk-discover-mini-ph"></span>';
|
|
1447
|
+
if(isImage(url)) return '<span class="rk-discover-mini"><img src="'+attr(url)+'" alt="" loading="lazy" decoding="async"></span>';
|
|
1448
|
+
// First-frame still, NOT autoplay: 4 tiles x 16 category cards would be
|
|
1449
|
+
// ~64 playing videos. preload=metadata + #t=0.1 paints frame one only.
|
|
1450
|
+
return '<span class="rk-discover-mini"><video src="'+attr(url+'#t=0.1')+'" muted playsinline preload="metadata" tabindex="-1" aria-hidden="true"></video></span>';
|
|
1451
|
+
}
|
|
1452
|
+
function showReveal(label){
|
|
1453
|
+
var items=groups[label]||[];
|
|
1454
|
+
if(!items.length) return false;
|
|
1455
|
+
revealItems=items;
|
|
1456
|
+
if(revealTitle) revealTitle.textContent=label;
|
|
1457
|
+
// cards carry data-rk-view=<i> so a plain click opens the shared viewer
|
|
1458
|
+
if(revealGrid) revealGrid.innerHTML=items.map(function(t,i){ return posterCardHtml(t,i); }).join('');
|
|
1459
|
+
if(catsGrid) catsGrid.hidden=true;
|
|
1460
|
+
if(reveal) reveal.hidden=false;
|
|
1461
|
+
if(revealHead) revealHead.hidden=false;
|
|
1462
|
+
window.scrollTo(0,0);
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
function hideReveal(){
|
|
1466
|
+
if(viewerCtl&&viewerCtl.isOpen()) viewerCtl.close();
|
|
1467
|
+
revealItems=[];
|
|
1468
|
+
if(reveal) reveal.hidden=true;
|
|
1469
|
+
if(revealHead) revealHead.hidden=true;
|
|
1470
|
+
if(catsGrid) catsGrid.hidden=false;
|
|
1471
|
+
}
|
|
1472
|
+
if(backBtn) backBtn.addEventListener('click',hideReveal);
|
|
1473
|
+
bindHoverAudio(revealGrid); // reveal cards share posterCardHtml → same hover audio
|
|
1474
|
+
bindViewerOpens(revealGrid, function(){ return revealItems; }); // click → shared TikTok viewer
|
|
1475
|
+
|
|
1476
|
+
// Intercept category-card clicks: if we have live templates for that group,
|
|
1477
|
+
// reveal them in-page; otherwise let the href fall through to a filtered
|
|
1478
|
+
// feed (graceful offline / not-yet-seeded fallback).
|
|
1479
|
+
catsRoot.querySelectorAll('[data-rk-cat]').forEach(function(a){
|
|
1480
|
+
a.addEventListener('click',function(e){
|
|
1481
|
+
var label=a.getAttribute('data-rk-cat');
|
|
1482
|
+
if(groups[label]&&groups[label].length){ e.preventDefault(); showReveal(label); }
|
|
1483
|
+
});
|
|
368
1484
|
});
|
|
1485
|
+
|
|
1486
|
+
function enrichCards(){
|
|
1487
|
+
catsRoot.querySelectorAll('[data-rk-cat]').forEach(function(a){
|
|
1488
|
+
var label=a.getAttribute('data-rk-cat');
|
|
1489
|
+
var items=groups[label]||[];
|
|
1490
|
+
if(!items.length) return;
|
|
1491
|
+
var thumbs=a.querySelector('[data-rk-cat-thumbs]');
|
|
1492
|
+
if(thumbs) thumbs.innerHTML=items.slice(0,4).map(miniHtml).join('');
|
|
1493
|
+
var count=a.querySelector('[data-rk-cat-count]');
|
|
1494
|
+
if(count) count.textContent=items.length+' template'+(items.length===1?'':'s');
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
function ingestPopular(list){
|
|
1498
|
+
groups={};
|
|
1499
|
+
(list||[]).forEach(function(t){ var g=(t.popularGroup||'Popular').trim()||'Popular'; (groups[g]=groups[g]||[]).push(t); });
|
|
1500
|
+
enrichCards();
|
|
1501
|
+
}
|
|
1502
|
+
function loadPopular(){
|
|
1503
|
+
var url=popularEndpoint;
|
|
1504
|
+
try{ var u=new URL(popularEndpoint, window.location.href); if(accountId) u.searchParams.set('account', accountId); url=u.toString(); }catch(e){ url=popularEndpoint; }
|
|
1505
|
+
fetch(url,{credentials:'same-origin',headers:{'accept':'application/json'}})
|
|
1506
|
+
.then(function(r){ if(!r.ok){ throw new Error("Couldn't load categories ("+r.status+")."); } return r.json(); })
|
|
1507
|
+
.then(function(j){
|
|
1508
|
+
var list=(j&&Array.isArray(j.templates))?j.templates:[];
|
|
1509
|
+
if(list.length){ ingestPopular(list); return; }
|
|
1510
|
+
// Local serve without the popular catalog seeded (and no upstream
|
|
1511
|
+
// passthrough configured): fall back to the public prod endpoint —
|
|
1512
|
+
// it's CORS-open and unauthenticated — so the sandbox still shows
|
|
1513
|
+
// real first-frame previews on a fully-local box.
|
|
1514
|
+
loadPopularFallback();
|
|
1515
|
+
// The static skeleton already shows every real group; the fetch just
|
|
1516
|
+
// enriches it, so a fetch that returns nothing is not an error state.
|
|
1517
|
+
})
|
|
1518
|
+
.catch(function(){
|
|
1519
|
+
// Offline / unreachable: try the public prod catalog before settling
|
|
1520
|
+
// for the static skeleton grid (real group labels + counts, clicks
|
|
1521
|
+
// fall through to the filtered feed).
|
|
1522
|
+
loadPopularFallback();
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
function loadPopularFallback(){
|
|
1526
|
+
if(window.location.hostname==='vidfarm.cc') return; // already prod
|
|
1527
|
+
fetch('https://vidfarm.cc/discover/popular',{headers:{'accept':'application/json'}})
|
|
1528
|
+
.then(function(r){ return r.ok?r.json():null; })
|
|
1529
|
+
.then(function(j){
|
|
1530
|
+
var list=(j&&Array.isArray(j.templates))?j.templates:[];
|
|
1531
|
+
if(list.length) ingestPopular(list);
|
|
1532
|
+
})
|
|
1533
|
+
.catch(function(){ /* keep the static skeleton */ });
|
|
1534
|
+
}
|
|
1535
|
+
loadPopular();
|
|
369
1536
|
}
|
|
370
1537
|
})();`;
|
|
371
1538
|
return reskinDocument({
|
|
372
1539
|
title: `vidfarm reskin — Discover (${mode})`,
|
|
373
|
-
description: "Reskinned vidfarm Discover — Feed / Swipe / Categories
|
|
1540
|
+
description: "Reskinned vidfarm Discover — real Feed / Swipe / Categories, live template posters, warm sunny SaaS.",
|
|
374
1541
|
activeSlug: "discover",
|
|
1542
|
+
account: { name: input.name, email: input.email },
|
|
375
1543
|
pageCss,
|
|
376
1544
|
body,
|
|
377
|
-
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
|
|
378
1549
|
});
|
|
379
1550
|
}
|
|
380
1551
|
//# sourceMappingURL=discover-page.js.map
|