@mevdragon/vidfarm-devcli 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/vidfarm-media/SKILL.md +3 -3
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/app.js +301 -45
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +502 -187
- package/dist/src/reskin/chat-page.js +470 -277
- package/dist/src/reskin/discover-page.js +988 -272
- package/dist/src/reskin/document.js +602 -13
- package/dist/src/reskin/help-page.js +138 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +435 -262
- package/dist/src/reskin/login-page.js +118 -81
- package/dist/src/reskin/pricing-page.js +195 -166
- package/dist/src/reskin/settings-page.js +520 -186
- package/dist/src/reskin/theme.js +106 -9
- package/package.json +1 -1
|
@@ -1,131 +1,60 @@
|
|
|
1
|
-
// /reskin/agency — the Agency / client-workspaces screen
|
|
2
|
-
// farmville design system.
|
|
3
|
-
// page-scoped CSS block + a self-contained module script.
|
|
1
|
+
// /reskin/agency — the Agency / client-workspaces screen, MIGRATED to real
|
|
2
|
+
// production data + behavior (farmville design system).
|
|
4
3
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
4
|
+
// This mirrors src/account-pages-legacy.ts → renderAgencyPage: an agency
|
|
5
|
+
// managing its client sub-accounts. The live page's client list is NOT
|
|
6
|
+
// server-fetched — it is read CLIENT-SIDE from this browser's localStorage
|
|
7
|
+
// account registry (the same cache the app writes when you sign into a client
|
|
8
|
+
// account), so the server can only render an empty shell + template and the
|
|
9
|
+
// real client cards are built in JS. This reskin replicates that exact read and
|
|
10
|
+
// wires the real per-client actions:
|
|
11
|
+
// • "Open workspace" → opens the client's /u/:id/library in a new tab
|
|
12
|
+
// • "Copy login" → copies the client's autologin URL
|
|
13
|
+
// • "Add client login"→ opens /agency-client-login
|
|
14
|
+
// localStorage contract (identical keys/fields to the live page):
|
|
15
|
+
// index key: "vidfarm:agency:accounts:index" → string[] of userIds
|
|
16
|
+
// entry key: "vidfarm:agency:account_<userId>" → { apiKey, email,
|
|
17
|
+
// displayName, lastSeenAt, autologinUrl? }
|
|
18
|
+
// autologinUrl falls back to "/auto-login?api_key=<encodeURIComponent(apiKey)>"
|
|
19
|
+
//
|
|
20
|
+
// The old reskin rendered fabricated per-client stats (projects, scheduled,
|
|
21
|
+
// wallet, seats, plan, status) and aggregate tiles — NONE of those live in the
|
|
22
|
+
// cache, so they are dropped. Cards now show only real cached fields (name,
|
|
23
|
+
// email, user id, last-seen). Avatars are text initials (no emoji).
|
|
13
24
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{ name: "Pixel Pantry", email: "founders@pixelpantry.app", userId: "usr_6e14d0", plan: "Starter", status: "trial", tint: "sky", projects: 1, scheduled: 1, walletUsd: 5.0, seats: 1, lastSeen: "1d ago" }
|
|
26
|
-
]
|
|
27
|
-
};
|
|
28
|
-
const STATUS_PILL = {
|
|
29
|
-
active: { cls: "rk-pill-green", label: "Active", dot: "●" },
|
|
30
|
-
trial: { cls: "rk-pill-sky", label: "Trial", dot: "◐" },
|
|
31
|
-
paused: { cls: "rk-pill-red", label: "Paused", dot: "◌" }
|
|
32
|
-
};
|
|
33
|
-
function money(n) {
|
|
34
|
-
const frac = Number.isInteger(n) ? 0 : 2;
|
|
35
|
-
return `$${n.toLocaleString("en-US", { minimumFractionDigits: frac, maximumFractionDigits: 2 })}`;
|
|
36
|
-
}
|
|
37
|
-
function initials(name) {
|
|
38
|
-
const letters = name
|
|
39
|
-
.replace(/&/g, " ")
|
|
40
|
-
.split(/\s+/)
|
|
41
|
-
.map((w) => (w.match(/[a-z0-9]/i) || [""])[0])
|
|
42
|
-
.filter(Boolean);
|
|
43
|
-
const out = `${letters[0] || ""}${letters[1] || ""}`;
|
|
44
|
-
return (out || "?").toUpperCase();
|
|
45
|
-
}
|
|
46
|
-
function statTile(label, value, sub) {
|
|
47
|
-
return `<div class="rk-card rk-card-pad-sm rk-agency-stat">
|
|
48
|
-
<div class="rk-stat-label">${rkEscape(label)}</div>
|
|
49
|
-
<div class="rk-stat-num">${value}</div>
|
|
50
|
-
<div class="rk-agency-stat-sub">${rkEscape(sub)}</div>
|
|
51
|
-
</div>`;
|
|
52
|
-
}
|
|
53
|
-
function clientCard(c) {
|
|
54
|
-
const pill = STATUS_PILL[c.status];
|
|
55
|
-
const search = `${c.name} ${c.email} ${c.userId}`.toLowerCase();
|
|
56
|
-
return `<article class="rk-card rk-card-hover rk-agency-card" data-rk-client="${rkEscape(search)}">
|
|
57
|
-
<div class="rk-agency-card-top">
|
|
58
|
-
<span class="rk-tile rk-tile-lg rk-tile-${c.tint} rk-agency-avatar">${rkEscape(initials(c.name))}</span>
|
|
59
|
-
<div class="rk-agency-card-id">
|
|
60
|
-
<h3 class="rk-agency-name">${rkEscape(c.name)}</h3>
|
|
61
|
-
<div class="rk-agency-email" role="button" tabindex="0" data-rk-copy-text="${rkEscape(c.email)}" title="Copy email">${rkEscape(c.email)}</div>
|
|
62
|
-
</div>
|
|
63
|
-
<span class="rk-pill ${pill.cls} rk-agency-status">${pill.dot} ${pill.label}</span>
|
|
64
|
-
</div>
|
|
65
|
-
<div class="rk-agency-meta">
|
|
66
|
-
<span class="rk-pill rk-pill-gold">${rkEscape(c.plan)} plan</span>
|
|
67
|
-
<span class="rk-pill">${c.seats} ${c.seats === 1 ? "seat" : "seats"}</span>
|
|
68
|
-
<span class="rk-agency-user" role="button" tabindex="0" data-rk-copy-text="${rkEscape(c.userId)}" title="Copy user id">${rkEscape(c.userId)}</span>
|
|
69
|
-
</div>
|
|
70
|
-
<div class="rk-agency-minis">
|
|
71
|
-
<div class="rk-agency-mini">
|
|
72
|
-
<div class="rk-agency-mini-num">${c.projects}</div>
|
|
73
|
-
<div class="rk-agency-mini-lbl">Projects</div>
|
|
74
|
-
</div>
|
|
75
|
-
<div class="rk-agency-mini">
|
|
76
|
-
<div class="rk-agency-mini-num">${c.scheduled}</div>
|
|
77
|
-
<div class="rk-agency-mini-lbl">Scheduled</div>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="rk-agency-mini">
|
|
80
|
-
<div class="rk-agency-mini-num">${money(c.walletUsd)}</div>
|
|
81
|
-
<div class="rk-agency-mini-lbl">Wallet</div>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
<div class="rk-agency-card-foot">
|
|
85
|
-
<a class="rk-btn rk-btn-ghost rk-btn-sm rk-agency-open" href="/u/${encodeURIComponent(c.userId)}/library" target="_blank" rel="noreferrer">Open workspace <span class="rk-arrow">→</span></a>
|
|
86
|
-
<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-copy-login="/auto-login?client=${encodeURIComponent(c.userId)}">Copy login</button>
|
|
87
|
-
<span class="rk-agency-seen">Active ${rkEscape(c.lastSeen)}</span>
|
|
88
|
-
</div>
|
|
89
|
-
</article>`;
|
|
90
|
-
}
|
|
91
|
-
export function renderReskinAgency() {
|
|
92
|
-
const a = SAMPLE;
|
|
93
|
-
const s = a.stats;
|
|
94
|
-
const stats = `
|
|
95
|
-
<div class="rk-grid rk-grid-4 rk-agency-stats">
|
|
96
|
-
${statTile("Client workspaces", String(s.clients), "all sub-accounts you manage")}
|
|
97
|
-
${statTile("Active projects", String(s.activeProjects), "across every client")}
|
|
98
|
-
${statTile("Seats used", `${s.seatsUsed}<span class="rk-agency-stat-of"> / ${s.seatsTotal}</span>`, "team members with access")}
|
|
99
|
-
${statTile("Monthly spend", money(s.monthlySpendUsd), "pooled render + clip usage")}
|
|
100
|
-
</div>`;
|
|
101
|
-
const clientCards = a.clients.map(clientCard).join("");
|
|
102
|
-
const inviteCard = `<article class="rk-card rk-agency-invite" data-rk-client="__invite__">
|
|
103
|
-
<span class="rk-tile rk-tile-lg rk-tile-gold rk-agency-invite-ic">+</span>
|
|
25
|
+
export function renderReskinAgency(input = { userId: null, currentAccountId: null, name: null, email: null, loggedIn: false }) {
|
|
26
|
+
const loggedIn = input.loggedIn ?? Boolean(input.userId);
|
|
27
|
+
const signedInHint = loggedIn && input.email
|
|
28
|
+
? `<span class="rk-agency-hint">Managing from ${rkEscape(input.email)}</span>`
|
|
29
|
+
: "";
|
|
30
|
+
const PLUS = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
31
|
+
// The invite / "add a client workspace" card is a static, always-real CTA to
|
|
32
|
+
// the /agency-client-login flow. It lives inside the grid alongside the
|
|
33
|
+
// JS-injected client cards (which render into [data-rk-agency-cards]).
|
|
34
|
+
const inviteCard = `<article class="rk-card rk-agency-invite" data-rk-agency-invite>
|
|
35
|
+
<span class="rk-tile rk-tile-lg rk-tile-gold rk-agency-invite-ic">${PLUS}</span>
|
|
104
36
|
<h3 class="rk-agency-invite-title">Add a client workspace</h3>
|
|
105
37
|
<p class="rk-muted rk-agency-invite-copy">Link a client's account and get an autologin so your team can post on their behalf.</p>
|
|
106
|
-
<
|
|
38
|
+
<a class="rk-btn rk-btn-gold" href="/agency-client-login" target="_blank" rel="noreferrer">Add client login <span class="rk-arrow">→</span></a>
|
|
107
39
|
</article>`;
|
|
108
40
|
const body = `
|
|
109
41
|
<main class="rk-container rk-page">
|
|
110
42
|
<div class="rk-page-head rk-agency-head">
|
|
111
43
|
<div class="rk-agency-head-copy">
|
|
112
|
-
<
|
|
113
|
-
<
|
|
114
|
-
<p class="rk-sub">Manage every client workspace from one seat — ${a.clients.length} accounts, shared seats, and one pooled bill.</p>
|
|
44
|
+
<h1 class="rk-h1">Agency clients</h1>
|
|
45
|
+
<p class="rk-sub">Every client workspace cached on this device — open their library, or copy a one-tap login to post on their behalf.</p>
|
|
115
46
|
</div>
|
|
116
47
|
<div class="rk-agency-head-cta">
|
|
117
|
-
|
|
118
|
-
<
|
|
48
|
+
${signedInHint}
|
|
49
|
+
<a class="rk-btn rk-btn-gold rk-btn-lg" href="/agency-client-login" target="_blank" rel="noreferrer">Add client login <span class="rk-arrow">→</span></a>
|
|
119
50
|
</div>
|
|
120
51
|
</div>
|
|
121
52
|
|
|
122
|
-
${stats}
|
|
123
|
-
|
|
124
53
|
<section class="rk-agency-panel">
|
|
125
54
|
<div class="rk-spread rk-wrap rk-agency-toolbar">
|
|
126
55
|
<div class="rk-agency-toolbar-copy">
|
|
127
56
|
<span class="rk-eyebrow">Clients</span>
|
|
128
|
-
<h2 class="rk-agency-h2"
|
|
57
|
+
<h2 class="rk-agency-h2"><span data-rk-agency-count>0</span> client workspace<span data-rk-agency-plural>s</span></h2>
|
|
129
58
|
</div>
|
|
130
59
|
<div class="rk-agency-search-wrap">
|
|
131
60
|
<span class="rk-agency-search-ic" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg></span>
|
|
@@ -133,27 +62,21 @@ export function renderReskinAgency() {
|
|
|
133
62
|
</div>
|
|
134
63
|
</div>
|
|
135
64
|
<div class="rk-grid rk-grid-3 rk-agency-grid" data-rk-agency-grid>
|
|
136
|
-
|
|
65
|
+
<div class="rk-agency-cards" data-rk-agency-cards></div>
|
|
137
66
|
${inviteCard}
|
|
138
67
|
</div>
|
|
139
68
|
<div class="rk-agency-empty" data-rk-agency-empty hidden>
|
|
140
|
-
<p class="rk-muted">No
|
|
69
|
+
<p class="rk-muted" data-rk-agency-empty-msg>No cached accounts found in this browser yet.</p>
|
|
141
70
|
</div>
|
|
142
71
|
</section>
|
|
143
72
|
</main>`;
|
|
144
73
|
const pageCss = `
|
|
145
74
|
.rk-agency-head{grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:20px}
|
|
146
|
-
.rk-agency-head-copy{display:grid;gap:
|
|
147
|
-
.rk-agency-head-
|
|
148
|
-
.rk-agency-
|
|
75
|
+
.rk-agency-head-copy{display:grid;gap:4px;min-width:0}
|
|
76
|
+
.rk-agency-head-cta{display:flex;align-items:center;gap:12px;flex:none}
|
|
77
|
+
.rk-agency-hint{font-size:13px;color:var(--rk-text-muted);white-space:nowrap}
|
|
149
78
|
@media(max-width:760px){.rk-agency-head{grid-template-columns:1fr}.rk-agency-head-cta{flex-wrap:wrap}}
|
|
150
79
|
|
|
151
|
-
.rk-agency-stats{margin-bottom:28px}
|
|
152
|
-
.rk-agency-stat{display:grid;gap:6px;align-content:start}
|
|
153
|
-
.rk-agency-stat-ic{display:grid;place-items:center;width:24px;height:24px;border-radius:8px;background:var(--rk-gold-tint);font-size:13px}
|
|
154
|
-
.rk-agency-stat-sub{font-size:12px;color:var(--rk-text-faint)}
|
|
155
|
-
.rk-agency-stat-of{font-size:var(--rk-text-xl);color:var(--rk-text-faint);font-weight:700}
|
|
156
|
-
|
|
157
80
|
.rk-agency-panel{background:var(--rk-bg-section);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);padding:24px}
|
|
158
81
|
.rk-agency-toolbar{margin-bottom:20px}
|
|
159
82
|
.rk-agency-toolbar-copy{display:grid;gap:4px}
|
|
@@ -164,6 +87,8 @@ export function renderReskinAgency() {
|
|
|
164
87
|
@media(max-width:620px){.rk-agency-search-wrap{width:100%}}
|
|
165
88
|
|
|
166
89
|
.rk-agency-grid{align-items:stretch}
|
|
90
|
+
/* the JS card wrapper is transparent to the grid so cards + invite share tracks */
|
|
91
|
+
.rk-agency-cards{display:contents}
|
|
167
92
|
.rk-agency-card{padding:22px;display:flex;flex-direction:column;gap:16px}
|
|
168
93
|
.rk-agency-card-top{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:start}
|
|
169
94
|
.rk-agency-avatar{font-family:var(--rk-font-display);font-weight:800;font-size:22px}
|
|
@@ -171,81 +96,200 @@ export function renderReskinAgency() {
|
|
|
171
96
|
.rk-agency-name{font-size:var(--rk-text-lg);font-weight:800;font-family:var(--rk-font-display);color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
172
97
|
.rk-agency-email{font-size:13px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;transition:color var(--rk-dur) var(--rk-ease)}
|
|
173
98
|
.rk-agency-email:hover{color:var(--rk-gold-700)}
|
|
174
|
-
.rk-agency-
|
|
175
|
-
.rk-agency-
|
|
176
|
-
.rk-agency-user{font-family:var(--rk-font-mono);font-size:12px;color:var(--rk-text-faint);cursor:pointer;transition:color var(--rk-dur) var(--rk-ease)}
|
|
99
|
+
.rk-agency-seen-pill{flex:none;align-self:start;white-space:nowrap}
|
|
100
|
+
.rk-agency-idrow{display:flex}
|
|
101
|
+
.rk-agency-user{font-family:var(--rk-font-mono);font-size:12px;color:var(--rk-text-faint);cursor:pointer;transition:color var(--rk-dur) var(--rk-ease);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
|
177
102
|
.rk-agency-user:hover{color:var(--rk-n-600)}
|
|
178
|
-
.rk-agency-
|
|
179
|
-
.rk-agency-mini{display:grid;gap:3px;justify-items:center;text-align:center;position:relative}
|
|
180
|
-
.rk-agency-mini+.rk-agency-mini::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);height:26px;width:1px;background:var(--rk-border)}
|
|
181
|
-
.rk-agency-mini-num{font-family:var(--rk-font-display);font-weight:800;font-size:1.1rem;color:var(--rk-ink);letter-spacing:-.02em}
|
|
182
|
-
.rk-agency-mini-lbl{font-size:11px;font-weight:600;color:var(--rk-text-muted);text-transform:uppercase;letter-spacing:.03em}
|
|
183
|
-
.rk-agency-card-foot{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
|
|
103
|
+
.rk-agency-card-foot{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:auto}
|
|
184
104
|
.rk-agency-open{font-weight:600}
|
|
185
|
-
.rk-agency-seen{margin-left:auto;font-size:12px;color:var(--rk-text-faint);white-space:nowrap}
|
|
186
105
|
|
|
187
106
|
.rk-agency-invite{border-style:dashed;border-color:var(--rk-border-strong);background:linear-gradient(180deg,var(--rk-gold-tint),#fff 70%);display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;min-height:240px}
|
|
188
|
-
.rk-agency-invite-ic{
|
|
107
|
+
.rk-agency-invite-ic{display:grid;place-items:center}
|
|
189
108
|
.rk-agency-invite-title{font-size:var(--rk-text-lg);font-family:var(--rk-font-display);font-weight:800;color:var(--rk-ink)}
|
|
190
109
|
.rk-agency-invite-copy{font-size:13px;max-width:26ch}
|
|
191
110
|
|
|
192
111
|
.rk-agency-empty{display:none;flex-direction:column;align-items:center;gap:12px;padding:56px 20px;text-align:center}
|
|
193
112
|
.rk-agency-empty[data-rk-shown="1"]{display:flex}
|
|
194
113
|
`;
|
|
114
|
+
// Client-side: replicate the live localStorage account-registry read, build
|
|
115
|
+
// real farmville client cards, and wire the real actions. Written with string
|
|
116
|
+
// concatenation (not template literals) to avoid nested-`${}` escaping.
|
|
195
117
|
const script = `
|
|
196
118
|
(function(){
|
|
119
|
+
var ACCOUNT_REGISTRY_INDEX_KEY = "vidfarm:agency:accounts:index";
|
|
120
|
+
var ACCOUNT_REGISTRY_ENTRY_PREFIX = "vidfarm:agency:account_";
|
|
121
|
+
var TINTS = ["gold","sky","violet","green","coral","ink"];
|
|
122
|
+
|
|
197
123
|
var root=document;
|
|
198
|
-
var
|
|
124
|
+
var cardsWrap=root.querySelector('[data-rk-agency-cards]');
|
|
125
|
+
var invite=root.querySelector('[data-rk-agency-invite]');
|
|
199
126
|
var search=root.querySelector('[data-rk-agency-search]');
|
|
200
127
|
var empty=root.querySelector('[data-rk-agency-empty]');
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
128
|
+
var emptyMsg=root.querySelector('[data-rk-agency-empty-msg]');
|
|
129
|
+
var countEl=root.querySelector('[data-rk-agency-count]');
|
|
130
|
+
var pluralEl=root.querySelector('[data-rk-agency-plural]');
|
|
131
|
+
var searchQuery="";
|
|
132
|
+
|
|
133
|
+
function escapeHtml(value){
|
|
134
|
+
return String(value==null?"":value)
|
|
135
|
+
.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")
|
|
136
|
+
.replace(/"/g,""").replace(/'/g,"'");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function initials(name){
|
|
140
|
+
var letters=String(name||"").replace(/&/g," ").split(/\\s+/)
|
|
141
|
+
.map(function(w){var m=w.match(/[a-z0-9]/i);return m?m[0]:"";})
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
var out=(letters[0]||"")+(letters[1]||"");
|
|
144
|
+
return (out||"?").toUpperCase();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function tintFor(seed){
|
|
148
|
+
var s=String(seed||""),h=0;
|
|
149
|
+
for(var i=0;i<s.length;i++){h=(h*31+s.charCodeAt(i))>>>0;}
|
|
150
|
+
return TINTS[h%TINTS.length];
|
|
216
151
|
}
|
|
217
|
-
|
|
218
|
-
|
|
152
|
+
|
|
153
|
+
function relTime(iso){
|
|
154
|
+
var t=Date.parse(iso||"");
|
|
155
|
+
if(isNaN(t))return "";
|
|
156
|
+
var diff=Date.now()-t; if(diff<0)diff=0;
|
|
157
|
+
var m=Math.floor(diff/60000);
|
|
158
|
+
if(m<1)return "just now";
|
|
159
|
+
if(m<60)return m+"m ago";
|
|
160
|
+
var hr=Math.floor(m/60);
|
|
161
|
+
if(hr<24)return hr+"h ago";
|
|
162
|
+
var d=Math.floor(hr/24);
|
|
163
|
+
if(d<7)return d+"d ago";
|
|
164
|
+
try{return new Date(t).toLocaleDateString("en-US",{month:"short",day:"numeric"});}catch(_){return d+"d ago";}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readAccountIds(){
|
|
168
|
+
try{
|
|
169
|
+
var raw=window.localStorage.getItem(ACCOUNT_REGISTRY_INDEX_KEY);
|
|
170
|
+
var parsed=raw?JSON.parse(raw):[];
|
|
171
|
+
return Array.isArray(parsed)?parsed.filter(function(v){return typeof v==="string"&&v;}):[];
|
|
172
|
+
}catch(_){return [];}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readAccounts(){
|
|
176
|
+
return readAccountIds().map(function(userId){
|
|
177
|
+
try{
|
|
178
|
+
var raw=window.localStorage.getItem(ACCOUNT_REGISTRY_ENTRY_PREFIX+userId);
|
|
179
|
+
if(!raw)return null;
|
|
180
|
+
var parsed=JSON.parse(raw);
|
|
181
|
+
if(!parsed||typeof parsed!=="object"||typeof parsed.apiKey!=="string"||!parsed.apiKey)return null;
|
|
182
|
+
return {
|
|
183
|
+
userId:userId,
|
|
184
|
+
email:typeof parsed.email==="string"?parsed.email:"",
|
|
185
|
+
displayName:typeof parsed.displayName==="string"?parsed.displayName:"",
|
|
186
|
+
lastSeenAt:typeof parsed.lastSeenAt==="string"?parsed.lastSeenAt:"",
|
|
187
|
+
autologinUrl:typeof parsed.autologinUrl==="string"?parsed.autologinUrl:"/auto-login?api_key="+encodeURIComponent(parsed.apiKey)
|
|
188
|
+
};
|
|
189
|
+
}catch(_){return null;}
|
|
190
|
+
}).filter(Boolean).sort(function(a,b){return Date.parse(b.lastSeenAt||"")-Date.parse(a.lastSeenAt||"");});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function matchesSearch(account){
|
|
194
|
+
var needle=searchQuery.trim().toLowerCase();
|
|
195
|
+
if(!needle)return true;
|
|
196
|
+
return [account.displayName,account.email,account.userId]
|
|
197
|
+
.some(function(v){return String(v||"").toLowerCase().indexOf(needle)!==-1;});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function cardHtml(account){
|
|
201
|
+
var title=account.displayName||account.email||account.userId;
|
|
202
|
+
var tint=tintFor(account.userId);
|
|
203
|
+
var seen=relTime(account.lastSeenAt);
|
|
204
|
+
var seenPill=seen?'<span class="rk-pill rk-agency-seen-pill">Active '+escapeHtml(seen)+'</span>':'';
|
|
205
|
+
var openTarget="/u/"+encodeURIComponent(account.userId)+"/library";
|
|
206
|
+
return ''
|
|
207
|
+
+'<article class="rk-card rk-card-hover rk-agency-card">'
|
|
208
|
+
+'<div class="rk-agency-card-top">'
|
|
209
|
+
+'<span class="rk-tile rk-tile-lg rk-tile-'+tint+' rk-agency-avatar">'+escapeHtml(initials(title))+'</span>'
|
|
210
|
+
+'<div class="rk-agency-card-id">'
|
|
211
|
+
+'<h3 class="rk-agency-name">'+escapeHtml(title)+'</h3>'
|
|
212
|
+
+'<div class="rk-agency-email" role="button" tabindex="0" data-rk-copy-text="'+escapeHtml(account.email||"")+'" title="Copy email">'+escapeHtml(account.email||"No email")+'</div>'
|
|
213
|
+
+'</div>'
|
|
214
|
+
+seenPill
|
|
215
|
+
+'</div>'
|
|
216
|
+
+'<div class="rk-agency-idrow">'
|
|
217
|
+
+'<span class="rk-agency-user" role="button" tabindex="0" data-rk-copy-text="'+escapeHtml(account.userId)+'" title="Copy user id">User '+escapeHtml(account.userId)+'</span>'
|
|
218
|
+
+'</div>'
|
|
219
|
+
+'<div class="rk-agency-card-foot">'
|
|
220
|
+
+'<button class="rk-btn rk-btn-ink rk-btn-sm rk-agency-open" type="button" data-rk-open-tab="'+escapeHtml(openTarget)+'">Open workspace <span class="rk-arrow">→</span></button>'
|
|
221
|
+
+'<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-copy-login="'+escapeHtml(account.autologinUrl)+'">Copy login</button>'
|
|
222
|
+
+'</div>'
|
|
223
|
+
+'</article>';
|
|
224
|
+
}
|
|
225
|
+
|
|
219
226
|
function flash(el,label){
|
|
220
|
-
|
|
227
|
+
if(!el)return;
|
|
221
228
|
var prev=el.textContent;
|
|
222
229
|
el.textContent=label;
|
|
223
|
-
setTimeout(function(){el.textContent=prev;},
|
|
230
|
+
window.setTimeout(function(){el.textContent=prev;},1300);
|
|
224
231
|
}
|
|
225
232
|
function copy(value,el){
|
|
226
|
-
if(!value
|
|
227
|
-
navigator.clipboard.writeText
|
|
233
|
+
if(!value)return;
|
|
234
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
235
|
+
navigator.clipboard.writeText(value).then(function(){flash(el,"Copied");},function(){flash(el,"Clipboard blocked");});
|
|
236
|
+
}else{flash(el,"Clipboard blocked");}
|
|
228
237
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
|
|
239
|
+
function wire(){
|
|
240
|
+
if(!cardsWrap)return;
|
|
241
|
+
cardsWrap.querySelectorAll('[data-rk-copy-text]').forEach(function(el){
|
|
242
|
+
var run=function(){copy(el.getAttribute('data-rk-copy-text'),el);};
|
|
243
|
+
el.addEventListener('click',run);
|
|
244
|
+
el.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();run();}});
|
|
245
|
+
});
|
|
246
|
+
cardsWrap.querySelectorAll('[data-rk-copy-login]').forEach(function(btn){
|
|
247
|
+
btn.addEventListener('click',function(){
|
|
248
|
+
var rel=btn.getAttribute('data-rk-copy-login')||'';
|
|
249
|
+
var abs=rel;try{abs=new URL(rel,window.location.origin).toString();}catch(_){}
|
|
250
|
+
copy(abs,btn);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
cardsWrap.querySelectorAll('[data-rk-open-tab]').forEach(function(btn){
|
|
254
|
+
btn.addEventListener('click',function(){
|
|
255
|
+
var target=btn.getAttribute('data-rk-open-tab');
|
|
256
|
+
if(target)window.open(target,'_blank','noopener,noreferrer');
|
|
257
|
+
});
|
|
239
258
|
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function render(){
|
|
262
|
+
if(!cardsWrap)return;
|
|
263
|
+
var accounts=readAccounts();
|
|
264
|
+
var total=accounts.length;
|
|
265
|
+
if(countEl)countEl.textContent=String(total);
|
|
266
|
+
if(pluralEl)pluralEl.textContent=total===1?'':'s';
|
|
267
|
+
|
|
268
|
+
var filtered=accounts.filter(matchesSearch);
|
|
269
|
+
cardsWrap.innerHTML=filtered.map(cardHtml).join('');
|
|
270
|
+
wire();
|
|
271
|
+
|
|
272
|
+
// hide the "add client" invite while actively searching (it isn't a result)
|
|
273
|
+
if(invite)invite.hidden=!!searchQuery.trim();
|
|
274
|
+
|
|
275
|
+
if(empty&&emptyMsg){
|
|
276
|
+
var showEmpty=false,msg='';
|
|
277
|
+
if(total===0){showEmpty=true;msg='No cached accounts found in this browser yet.';}
|
|
278
|
+
else if(filtered.length===0){showEmpty=true;msg='No agency clients match that search.';}
|
|
279
|
+
emptyMsg.textContent=msg;
|
|
280
|
+
empty.hidden=!showEmpty;
|
|
281
|
+
empty.setAttribute('data-rk-shown',showEmpty?'1':'0');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if(search){
|
|
286
|
+
search.addEventListener('input',function(){searchQuery=search.value||'';render();});
|
|
287
|
+
}
|
|
288
|
+
render();
|
|
245
289
|
})();`;
|
|
246
290
|
return reskinDocument({
|
|
247
291
|
title: "vidfarm reskin — Agency",
|
|
248
|
-
description: "Reskinned vidfarm agency page
|
|
292
|
+
description: "Reskinned vidfarm agency page — cached client workspaces, open library, copy autologin.",
|
|
249
293
|
activeSlug: "agency",
|
|
250
294
|
pageCss,
|
|
251
295
|
body,
|