@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,29 +1,48 @@
|
|
|
1
|
-
// /reskin/calendar — the scheduled-posts calendar
|
|
2
|
-
// design system. A calm, full-width month board where each day holds simple
|
|
3
|
-
// post chips (platform swatch + title + time) with a small status dot, today
|
|
4
|
-
// highlighted, and a compact status legend beneath the grid.
|
|
1
|
+
// /reskin/calendar — the scheduled-posts calendar, MIGRATED to real production data.
|
|
5
2
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// Mirrors the live /calendar page's two-column layout: a LEFT rail listing the
|
|
4
|
+
// connected channels (searchable, click to filter the board to one channel) and
|
|
5
|
+
// a compact month board on the RIGHT sized to the viewport so the whole month
|
|
6
|
+
// is visible without scrolling.
|
|
10
7
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// Like the live page, this renders the shell server-side, then CLIENT-FETCHES
|
|
9
|
+
// the real posts from `input.loadEndpoint` (`/calendar/feed`, same-origin,
|
|
10
|
+
// credentials included) and drops them onto the correct day cells. It accepts
|
|
11
|
+
// the SAME input object the live route already builds (via
|
|
12
|
+
// `Parameters<typeof renderCalendarPage>[0]`).
|
|
13
|
+
//
|
|
14
|
+
// /calendar/feed response contract (see serializeScheduleCalendarPost in app.ts):
|
|
15
|
+
// {
|
|
16
|
+
// channels: [{ id, name, handle, platform, avatarUrl, platformIconUrl? }],
|
|
17
|
+
// posts: [{ id, channelId, scheduledAt (ISO), status (label),
|
|
18
|
+
// statusTone: "draft"|"review"|"ready"|"scheduled"|"sent"|"error",
|
|
19
|
+
// title, summary, kind, url }],
|
|
20
|
+
// startDate, endDate, syncStatus, error?
|
|
21
|
+
// }
|
|
22
|
+
// The client sends `?start_date=&end_date=` for the visible month range; posts map
|
|
23
|
+
// onto days by the local date of `scheduledAt`. Email channel ids are `email:<id>`
|
|
24
|
+
// and match `post.channelId`, so platform is resolved via the channels lookup.
|
|
25
|
+
//
|
|
26
|
+
// Design stays farmville (shared rk-* primitives + a page-scoped rk-calendar- block);
|
|
27
|
+
// NO decorative emoji — platform swatches use letter initials (T/I/Y/E/…).
|
|
14
28
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
// A safe default so calling with no/partial args never throws. The live route
|
|
30
|
+
// always passes a fully-populated object (posts seeded empty — they arrive from
|
|
31
|
+
// the feed), so this default only matters for previews / direct calls.
|
|
32
|
+
const DEFAULT_INPUT = {
|
|
33
|
+
userId: "",
|
|
34
|
+
currentAccountId: "",
|
|
35
|
+
name: null,
|
|
36
|
+
email: "",
|
|
37
|
+
loadEndpoint: "/calendar/feed",
|
|
38
|
+
hasFlockPosterKey: false,
|
|
39
|
+
connectChannelsHref: "/settings?tab=channels&connect=flockposter",
|
|
40
|
+
channels: [],
|
|
41
|
+
posts: []
|
|
26
42
|
};
|
|
43
|
+
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
44
|
+
const SEARCH_ICON = `<svg width="15" height="15" 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>`;
|
|
45
|
+
// statusTone → { label, pill } for the chip tooltip + the legend dots.
|
|
27
46
|
const STATUS = {
|
|
28
47
|
draft: { label: "Draft", pill: "rk-pill" },
|
|
29
48
|
review: { label: "Review", pill: "rk-pill-gold" },
|
|
@@ -32,221 +51,517 @@ const STATUS = {
|
|
|
32
51
|
sent: { label: "Sent", pill: "rk-pill-violet" },
|
|
33
52
|
error: { label: "Failed", pill: "rk-pill-red" }
|
|
34
53
|
};
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
7: [
|
|
44
|
-
{ title: "Feature reveal teaser", platform: "Instagram", status: "ready", time: "11:00 AM" },
|
|
45
|
-
{ title: "Newsletter: ship week", platform: "Email", status: "scheduled", time: "4:00 PM" },
|
|
46
|
-
{ title: "POV: launch day", platform: "TikTok", status: "scheduled", time: "6:00 PM" }
|
|
47
|
-
],
|
|
48
|
-
8: [{ title: "Testimonial cut", platform: "YouTube", status: "scheduled", time: "1:00 PM" }],
|
|
49
|
-
9: [{ title: "Duet reply to @acme", platform: "TikTok", status: "draft", time: "5:15 PM" }],
|
|
50
|
-
10: [
|
|
51
|
-
{ title: "Weekly recap", platform: "Email", status: "scheduled", time: "9:00 AM" },
|
|
52
|
-
{ title: "Founder Q&A reel", platform: "Instagram", status: "review", time: "3:00 PM" }
|
|
53
|
-
],
|
|
54
|
-
13: [{ title: "Hook test v4", platform: "TikTok", status: "scheduled", time: "8:00 AM" }],
|
|
55
|
-
14: [
|
|
56
|
-
{ title: "Case study reel", platform: "Instagram", status: "scheduled", time: "12:00 PM" },
|
|
57
|
-
{ title: "Shorts: 60s explainer", platform: "YouTube", status: "ready", time: "5:00 PM" }
|
|
58
|
-
],
|
|
59
|
-
15: [{ title: "Mid-month drop", platform: "Email", status: "sent", time: "9:00 AM" }],
|
|
60
|
-
16: [{ title: "Trend jack: sound #4127", platform: "TikTok", status: "error", time: "7:00 PM" }],
|
|
61
|
-
17: [{ title: "Behind the numbers", platform: "YouTube", status: "scheduled", time: "2:00 PM" }],
|
|
62
|
-
20: [
|
|
63
|
-
{ title: "Monday reset", platform: "TikTok", status: "scheduled", time: "8:30 AM" },
|
|
64
|
-
{ title: "Carousel: 5 tips", platform: "Instagram", status: "draft", time: "1:00 PM" }
|
|
65
|
-
],
|
|
66
|
-
21: [{ title: "Founder story pt.2", platform: "Instagram", status: "scheduled", time: "11:00 AM" }],
|
|
67
|
-
23: [{ title: "Live recap", platform: "YouTube", status: "review", time: "4:00 PM" }],
|
|
68
|
-
24: [
|
|
69
|
-
{ title: "Weekly drop", platform: "Email", status: "scheduled", time: "9:00 AM" },
|
|
70
|
-
{ title: "Friday feature", platform: "TikTok", status: "scheduled", time: "5:00 PM" }
|
|
71
|
-
],
|
|
72
|
-
27: [{ title: "Hook test v5", platform: "TikTok", status: "scheduled", time: "8:00 AM" }],
|
|
73
|
-
28: [
|
|
74
|
-
{ title: "Product deep-dive", platform: "YouTube", status: "scheduled", time: "1:00 PM" },
|
|
75
|
-
{ title: "Reel: real results", platform: "Instagram", status: "ready", time: "6:00 PM" }
|
|
76
|
-
],
|
|
77
|
-
30: [
|
|
78
|
-
{ title: "Month-end recap", platform: "Email", status: "scheduled", time: "10:00 AM" },
|
|
79
|
-
{ title: "TikTok trend remix", platform: "TikTok", status: "draft", time: "3:00 PM" },
|
|
80
|
-
{ title: "Teaser: August drop", platform: "Instagram", status: "scheduled", time: "7:00 PM" }
|
|
81
|
-
],
|
|
82
|
-
31: [{ title: "Final Friday push", platform: "TikTok", status: "scheduled", time: "5:00 PM" }]
|
|
83
|
-
};
|
|
84
|
-
const CHANNELS = [
|
|
85
|
-
{ name: "Vidfarm Daily", handle: "@vidfarm", platform: "TikTok", active: true },
|
|
86
|
-
{ name: "Founder Reels", handle: "@dylan.builds", platform: "Instagram" },
|
|
87
|
-
{ name: "Studio Shorts", handle: "@vidfarm.studio", platform: "YouTube" },
|
|
88
|
-
{ name: "Weekly Drop", handle: "dylan@studio.co", platform: "Email" }
|
|
89
|
-
];
|
|
90
|
-
function allPosts() {
|
|
91
|
-
const out = [];
|
|
92
|
-
for (const [dom, posts] of Object.entries(POSTS_BY_DOM)) {
|
|
93
|
-
for (const p of posts)
|
|
94
|
-
out.push({ ...p, dom: Number(dom) });
|
|
95
|
-
}
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
function platformSwatch(platform) {
|
|
99
|
-
const meta = PLATFORM[platform];
|
|
100
|
-
return `<span class="rk-calendar-swatch rk-calendar-swatch-${meta.swatch}" aria-hidden="true">${meta.initial}</span>`;
|
|
101
|
-
}
|
|
102
|
-
// One calm chip: platform swatch + title + a small status dot beside the time.
|
|
103
|
-
// No colored borders — status lives in the dot (mapped by the legend below).
|
|
104
|
-
function chip(p) {
|
|
105
|
-
const st = STATUS[p.status];
|
|
106
|
-
const meta = PLATFORM[p.platform];
|
|
107
|
-
return `<button type="button" class="rk-calendar-chip" title="${rkEscape(`${p.title} · ${meta.label} · ${st.label} · ${p.time}`)}">
|
|
108
|
-
${platformSwatch(p.platform)}
|
|
109
|
-
<span class="rk-calendar-chip-body">
|
|
110
|
-
<span class="rk-calendar-chip-title">${rkEscape(p.title)}</span>
|
|
111
|
-
<span class="rk-calendar-chip-foot">
|
|
112
|
-
<span class="rk-calendar-dot" data-status="${p.status}" aria-hidden="true"></span>
|
|
113
|
-
<span class="rk-calendar-chip-time">${rkEscape(p.time)}</span>
|
|
114
|
-
</span>
|
|
115
|
-
</span>
|
|
116
|
-
</button>`;
|
|
117
|
-
}
|
|
118
|
-
function monthCells() {
|
|
119
|
-
const firstWeekday = new Date(Date.UTC(YEAR, MONTH, 1)).getUTCDay();
|
|
120
|
-
const daysInMonth = new Date(Date.UTC(YEAR, MONTH + 1, 0)).getUTCDate();
|
|
54
|
+
// Render a bare month-grid skeleton server-side (based on `today`) so the board
|
|
55
|
+
// paints immediately; the client rebuilds it and fills in the real posts.
|
|
56
|
+
function skeletonCells() {
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const year = now.getUTCFullYear();
|
|
59
|
+
const month = now.getUTCMonth();
|
|
60
|
+
const firstWeekday = new Date(Date.UTC(year, month, 1)).getUTCDay();
|
|
61
|
+
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
121
62
|
const rows = Math.ceil((firstWeekday + daysInMonth) / 7);
|
|
122
63
|
const total = rows * 7;
|
|
64
|
+
const todayDom = now.getUTCDate();
|
|
123
65
|
const cells = [];
|
|
124
66
|
for (let i = 0; i < total; i++) {
|
|
125
|
-
const offset = i - firstWeekday;
|
|
126
|
-
const d = new Date(Date.UTC(
|
|
127
|
-
const inMonth = d.getUTCMonth() ===
|
|
67
|
+
const offset = i - firstWeekday;
|
|
68
|
+
const d = new Date(Date.UTC(year, month, 1 + offset));
|
|
69
|
+
const inMonth = d.getUTCMonth() === month;
|
|
128
70
|
const dom = d.getUTCDate();
|
|
129
|
-
const isToday = inMonth && dom ===
|
|
130
|
-
const posts = inMonth ? POSTS_BY_DOM[dom] ?? [] : [];
|
|
131
|
-
const shown = posts.slice(0, 3);
|
|
132
|
-
const overflow = posts.length - shown.length;
|
|
71
|
+
const isToday = inMonth && dom === todayDom;
|
|
133
72
|
const cls = ["rk-calendar-cell"];
|
|
134
73
|
if (!inMonth)
|
|
135
74
|
cls.push("is-out");
|
|
136
75
|
if (isToday)
|
|
137
76
|
cls.push("is-today");
|
|
138
|
-
const badge = isToday ? `<span class="rk-calendar-today">Today</span>` : "";
|
|
139
|
-
const chips = shown.map(chip).join("");
|
|
140
|
-
const more = overflow > 0 ? `<span class="rk-calendar-more">+${overflow} more</span>` : "";
|
|
141
77
|
cells.push(`<div class="${cls.join(" ")}">
|
|
142
78
|
<div class="rk-calendar-cell-head">
|
|
143
79
|
<span class="rk-calendar-daynum">${dom}</span>
|
|
144
|
-
${badge}
|
|
145
80
|
</div>
|
|
146
|
-
<div class="rk-calendar-cell-posts"
|
|
81
|
+
<div class="rk-calendar-cell-posts"></div>
|
|
147
82
|
</div>`);
|
|
148
83
|
}
|
|
149
84
|
return cells.join("");
|
|
150
85
|
}
|
|
151
|
-
export function renderReskinCalendar() {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
//
|
|
86
|
+
export function renderReskinCalendar(input = DEFAULT_INPUT) {
|
|
87
|
+
const cfg = { ...DEFAULT_INPUT, ...input };
|
|
88
|
+
// "Schedule post" → the real destination the live app uses. Scheduling a post
|
|
89
|
+
// happens from the Library's schedule modal (POST /approved/posts/:id/schedules);
|
|
90
|
+
// if no channels are connected yet, the prerequisite is to connect them first.
|
|
91
|
+
const acctPrefix = /^\/u\/[^/]+/.exec(cfg.connectChannelsHref)?.[0] ?? "";
|
|
92
|
+
const scheduleHref = cfg.hasFlockPosterKey ? `${acctPrefix}/library` : cfg.connectChannelsHref;
|
|
93
|
+
const scheduleLabel = cfg.hasFlockPosterKey ? "Schedule post" : "Connect a channel";
|
|
155
94
|
const weekdayHead = WEEKDAYS.map((w) => `<div class="rk-calendar-weekday">${w}</div>`).join("");
|
|
156
|
-
// compact one-line status legend under the grid (explains the chip dots)
|
|
157
95
|
const legendOrder = ["draft", "review", "ready", "scheduled", "sent", "error"];
|
|
158
96
|
const legend = legendOrder.map((s) => `<span class="rk-calendar-legend-item"><span class="rk-calendar-dot" data-status="${s}"></span>${STATUS[s].label}</span>`).join("");
|
|
97
|
+
// seed + config handed to the client (posts arrive from the feed).
|
|
98
|
+
const boot = {
|
|
99
|
+
loadEndpoint: cfg.loadEndpoint,
|
|
100
|
+
hasChannels: cfg.hasFlockPosterKey,
|
|
101
|
+
connectChannelsHref: cfg.connectChannelsHref,
|
|
102
|
+
seedChannels: cfg.channels,
|
|
103
|
+
seedPosts: cfg.posts
|
|
104
|
+
};
|
|
105
|
+
const bootJson = JSON.stringify(boot).replace(/</g, "\\u003c");
|
|
159
106
|
const body = `
|
|
160
|
-
<main class="rk-container-wide rk-page">
|
|
161
|
-
<div class="rk-page-head">
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<div class="rk-calendar-toolbar">
|
|
168
|
-
<div class="rk-calendar-monthnav">
|
|
169
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" aria-label="Previous month">←</button>
|
|
170
|
-
<span class="rk-calendar-month">${MONTH_LABEL}</span>
|
|
171
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" aria-label="Next month">→</button>
|
|
172
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-calendar-today-btn">Today</button>
|
|
107
|
+
<main class="rk-container-wide rk-page rk-calendar-page">
|
|
108
|
+
<div class="rk-spread rk-page-head rk-calendar-head">
|
|
109
|
+
<div class="rk-stack rk-calendar-head-copy">
|
|
110
|
+
<h1 class="rk-h1">Calendar</h1>
|
|
111
|
+
<p class="rk-sub" data-rk-cal-sub>Your scheduled short-form posts across every connected channel.</p>
|
|
173
112
|
</div>
|
|
174
|
-
<
|
|
113
|
+
<a class="rk-btn rk-btn-gold rk-btn-sm rk-calendar-cta" href="${rkEscape(scheduleHref)}">${rkEscape(scheduleLabel)} <span class="rk-arrow">→</span></a>
|
|
175
114
|
</div>
|
|
176
115
|
|
|
177
|
-
<div class="rk-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
116
|
+
<div class="rk-calendar-notice" data-rk-cal-notice hidden></div>
|
|
117
|
+
|
|
118
|
+
<div class="rk-calendar-shell">
|
|
119
|
+
<aside class="rk-calendar-rail" aria-label="Connected channels">
|
|
120
|
+
<label class="rk-calendar-chsearch">
|
|
121
|
+
<span class="rk-calendar-chsearch-ic" aria-hidden="true">${SEARCH_ICON}</span>
|
|
122
|
+
<input class="rk-input rk-calendar-chsearch-input" data-rk-cal-chsearch type="search"
|
|
123
|
+
placeholder="Search channels" autocomplete="off" aria-label="Search channels" />
|
|
124
|
+
</label>
|
|
125
|
+
<div class="rk-calendar-channels" data-rk-cal-channels></div>
|
|
126
|
+
<a class="rk-calendar-connect" href="${rkEscape(cfg.connectChannelsHref)}">+ Connect a channel</a>
|
|
127
|
+
</aside>
|
|
128
|
+
|
|
129
|
+
<section class="rk-calendar-stage">
|
|
130
|
+
<div class="rk-calendar-toolbar">
|
|
131
|
+
<div class="rk-calendar-monthnav">
|
|
132
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" data-rk-cal-prev aria-label="Previous month">←</button>
|
|
133
|
+
<span class="rk-calendar-month" data-rk-cal-month> </span>
|
|
134
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" data-rk-cal-next aria-label="Next month">→</button>
|
|
135
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-calendar-today-btn" data-rk-cal-today>Today</button>
|
|
136
|
+
<span class="rk-calendar-sync" data-rk-cal-sync aria-live="polite"></span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="rk-calendar-legend">${legend}</div>
|
|
139
|
+
</div>
|
|
183
140
|
|
|
184
|
-
|
|
185
|
-
</
|
|
141
|
+
<div class="rk-card rk-calendar-grid-card">
|
|
142
|
+
<div class="rk-calendar-weekdays">${weekdayHead}</div>
|
|
143
|
+
<div class="rk-calendar-grid" data-rk-cal-grid>${skeletonCells()}</div>
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
</main>
|
|
148
|
+
<script type="application/json" data-rk-cal-boot>${bootJson}</script>`;
|
|
186
149
|
const pageCss = `
|
|
187
|
-
.rk-
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
.rk-calendar-
|
|
191
|
-
.rk-calendar-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.rk-calendar-
|
|
197
|
-
.rk-calendar-
|
|
198
|
-
|
|
199
|
-
|
|
150
|
+
/* .rk-page zeroes horizontal padding; the wide container == viewport at 1280 so
|
|
151
|
+
margin:auto adds no gutter. Restore side gutters (longhand beats the shorthand),
|
|
152
|
+
and trim the bottom so the whole month fits in one viewport. */
|
|
153
|
+
.rk-calendar-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter);padding-bottom:28px}
|
|
154
|
+
.rk-calendar-head{align-items:flex-start;margin-bottom:14px}
|
|
155
|
+
.rk-calendar-head-copy{gap:4px;margin:0;flex:1 1 auto;min-width:0}
|
|
156
|
+
.rk-calendar-cta{flex:none;margin-top:2px}
|
|
157
|
+
|
|
158
|
+
/* notice banner (errors / empty warnings) */
|
|
159
|
+
.rk-calendar-notice{margin-bottom:12px}
|
|
160
|
+
.rk-calendar-notice[hidden]{display:none}
|
|
161
|
+
|
|
162
|
+
/* two-column shell: channel rail left, compact month board right */
|
|
163
|
+
.rk-calendar-shell{display:grid;grid-template-columns:280px minmax(0,1fr);gap:16px;align-items:start}
|
|
164
|
+
@media(max-width:960px){.rk-calendar-shell{grid-template-columns:1fr}}
|
|
165
|
+
|
|
166
|
+
/* ── channel rail ── */
|
|
167
|
+
.rk-calendar-rail{display:grid;gap:10px;background:#fff;border:1px solid var(--rk-border);
|
|
168
|
+
border-radius:var(--rk-r-2xl);padding:12px;box-shadow:var(--rk-shadow-card)}
|
|
169
|
+
.rk-calendar-chsearch{position:relative;display:block}
|
|
170
|
+
.rk-calendar-chsearch-ic{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
|
|
171
|
+
.rk-calendar-chsearch-input{padding:9px 12px 9px 37px;border-radius:var(--rk-r-full);font-size:13.5px}
|
|
172
|
+
.rk-calendar-channels{display:grid;gap:6px;max-height:calc(100vh - 320px);min-height:120px;overflow:auto}
|
|
173
|
+
.rk-calendar-channel{display:flex;align-items:center;gap:10px;width:100%;padding:9px 10px;border:1px solid transparent;
|
|
174
|
+
border-radius:var(--rk-r-xl);background:transparent;cursor:pointer;text-align:left;font-family:var(--rk-font-body);
|
|
175
|
+
transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
176
|
+
.rk-calendar-channel:hover{background:var(--rk-n-100)}
|
|
177
|
+
.rk-calendar-channel.is-selected{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
178
|
+
.rk-calendar-channel-avatar{position:relative;width:34px;height:34px;border-radius:var(--rk-r-full);flex:none;overflow:visible}
|
|
179
|
+
.rk-calendar-channel-avatar img{width:100%;height:100%;border-radius:var(--rk-r-full);object-fit:cover;display:block;
|
|
180
|
+
border:1px solid var(--rk-border)}
|
|
181
|
+
.rk-calendar-channel-initials{width:100%;height:100%;border-radius:var(--rk-r-full);display:grid;place-items:center;
|
|
182
|
+
background:var(--rk-n-800);color:#fff;font-family:var(--rk-font-display);font-weight:800;font-size:12px;letter-spacing:.03em}
|
|
183
|
+
.rk-calendar-channel-main{display:grid;gap:1px;min-width:0;flex:1}
|
|
184
|
+
.rk-calendar-channel-name{font-size:13.5px;font-weight:700;color:var(--rk-ink);letter-spacing:-.01em;
|
|
185
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
186
|
+
.rk-calendar-channel-sub{font-size:11.5px;font-weight:500;color:var(--rk-text-muted);
|
|
187
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
188
|
+
.rk-calendar-channel-count{flex:none;font-size:11px;font-weight:700;color:var(--rk-text-muted);
|
|
189
|
+
background:var(--rk-n-100);border-radius:var(--rk-r-full);padding:2px 8px}
|
|
190
|
+
.rk-calendar-channel.is-selected .rk-calendar-channel-count{background:#fff}
|
|
191
|
+
.rk-calendar-channels-empty{padding:14px 10px;font-size:12.5px;color:var(--rk-text-muted);line-height:1.5}
|
|
192
|
+
.rk-calendar-connect{display:block;text-align:center;padding:9px 12px;border:1.5px dashed var(--rk-border-strong);
|
|
193
|
+
border-radius:var(--rk-r-xl);font-size:13px;font-weight:600;color:var(--rk-text-muted);
|
|
194
|
+
transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
195
|
+
.rk-calendar-connect:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink)}
|
|
196
|
+
|
|
197
|
+
/* platform swatch badge on the avatar — letter initial, no emoji */
|
|
198
|
+
.rk-calendar-swatch{position:absolute;right:-3px;bottom:-3px;width:16px;height:16px;border-radius:6px;
|
|
199
|
+
display:grid;place-items:center;font-family:var(--rk-font-display);font-size:9px;font-weight:800;line-height:1;
|
|
200
|
+
box-shadow:var(--rk-shadow-xs);border:1.5px solid #fff}
|
|
201
|
+
.rk-calendar-swatch-tiktok{background:var(--rk-ink);color:#fff}
|
|
202
|
+
.rk-calendar-swatch-instagram{background:linear-gradient(135deg,#f9ce34,#ee2a7b 55%,#6228d7);color:#fff}
|
|
203
|
+
.rk-calendar-swatch-youtube{background:#ff3b30;color:#fff}
|
|
204
|
+
.rk-calendar-swatch-email{background:var(--rk-sky-tint);color:#1d6fb8}
|
|
205
|
+
.rk-calendar-swatch-generic{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
|
|
206
|
+
|
|
207
|
+
/* ── stage: toolbar + board ── */
|
|
208
|
+
.rk-calendar-stage{display:grid;gap:10px;min-width:0}
|
|
209
|
+
.rk-calendar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
|
|
210
|
+
.rk-calendar-monthnav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
211
|
+
.rk-calendar-month{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);letter-spacing:-.02em;color:var(--rk-ink);min-width:7.4rem;text-align:center}
|
|
212
|
+
.rk-calendar-navbtn{width:32px;height:32px;padding:0;font-size:14px;line-height:1}
|
|
213
|
+
.rk-calendar-today-btn{margin-left:2px;padding:5px 11px;font-size:12.5px}
|
|
214
|
+
.rk-calendar-sync{font-size:12px;font-weight:600;color:var(--rk-text-muted);margin-left:4px}
|
|
215
|
+
.rk-calendar-sync.is-error{color:var(--rk-red)}
|
|
216
|
+
|
|
217
|
+
/* compact one-line legend, lives in the toolbar */
|
|
218
|
+
.rk-calendar-legend{display:flex;flex-wrap:wrap;gap:12px}
|
|
219
|
+
.rk-calendar-legend-item{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;color:var(--rk-text-muted)}
|
|
220
|
+
|
|
221
|
+
/* month board — sized to the viewport so the whole month is visible */
|
|
222
|
+
.rk-calendar-grid-card{padding:0;overflow:hidden;position:relative;display:flex;flex-direction:column;
|
|
223
|
+
height:calc(100vh - 218px);min-height:430px}
|
|
224
|
+
@media(max-width:960px){.rk-calendar-grid-card{height:auto;min-height:0}}
|
|
225
|
+
.rk-calendar-weekdays{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));background:var(--rk-n-50);border-bottom:1px solid var(--rk-border);flex:none}
|
|
226
|
+
.rk-calendar-weekday{padding:8px 8px;text-align:center;font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--rk-text-muted)}
|
|
227
|
+
.rk-calendar-grid{flex:1;min-height:0;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));grid-auto-rows:1fr;
|
|
228
|
+
gap:1px;background:var(--rk-border);transition:opacity var(--rk-dur) var(--rk-ease)}
|
|
229
|
+
.rk-calendar-grid.is-loading{opacity:.55}
|
|
230
|
+
.rk-calendar-cell{background:#fff;min-height:0;padding:6px 7px;display:flex;flex-direction:column;gap:4px;overflow:hidden;
|
|
231
|
+
transition:background var(--rk-dur) var(--rk-ease)}
|
|
232
|
+
@media(max-width:960px){.rk-calendar-cell{min-height:92px}}
|
|
200
233
|
.rk-calendar-cell:hover{background:var(--rk-n-50)}
|
|
201
234
|
.rk-calendar-cell.is-out{background:var(--rk-n-50)}
|
|
202
235
|
.rk-calendar-cell.is-out:hover{background:var(--rk-n-100)}
|
|
203
236
|
.rk-calendar-cell.is-today{background:var(--rk-gold-tint);box-shadow:inset 0 0 0 2px var(--rk-gold-500)}
|
|
204
|
-
.rk-calendar-cell-head{display:flex;align-items:center;justify-content:space-between;gap:
|
|
205
|
-
.rk-calendar-daynum{width:
|
|
237
|
+
.rk-calendar-cell-head{display:flex;align-items:center;justify-content:space-between;gap:4px;flex:none}
|
|
238
|
+
.rk-calendar-daynum{width:22px;height:22px;border-radius:var(--rk-r-full);display:grid;place-items:center;
|
|
239
|
+
font-family:var(--rk-font-display);font-weight:700;font-size:12px;color:var(--rk-n-700)}
|
|
206
240
|
.rk-calendar-cell.is-out .rk-calendar-daynum{color:var(--rk-n-400);font-weight:600}
|
|
207
241
|
.rk-calendar-cell.is-today .rk-calendar-daynum{background:var(--rk-gold-500);color:var(--rk-ink);box-shadow:var(--rk-shadow-xs)}
|
|
208
|
-
.rk-calendar-
|
|
209
|
-
.rk-calendar-cell-posts{display:flex;flex-direction:column;gap:7px}
|
|
242
|
+
.rk-calendar-cell-posts{display:flex;flex-direction:column;gap:3px;min-height:0;overflow:hidden}
|
|
210
243
|
|
|
211
|
-
/* post chip —
|
|
212
|
-
.rk-calendar-chip{display:flex;gap:
|
|
244
|
+
/* post chip — one compact line: status dot · time · title */
|
|
245
|
+
.rk-calendar-chip{display:flex;gap:6px;align-items:center;width:100%;text-align:left;padding:3px 7px;border-radius:8px;
|
|
246
|
+
background:#fff;border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);cursor:pointer;min-width:0;flex:none;
|
|
247
|
+
transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease);text-decoration:none;color:inherit}
|
|
213
248
|
.rk-calendar-chip:hover{transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
|
|
214
|
-
.rk-calendar-chip-
|
|
215
|
-
.rk-calendar-chip-title{font-size:
|
|
216
|
-
|
|
217
|
-
.rk-calendar-
|
|
218
|
-
.rk-calendar-more{font-size:11px;font-weight:700;color:var(--rk-text-muted);padding:1px 2px}
|
|
219
|
-
|
|
220
|
-
/* platform swatch */
|
|
221
|
-
.rk-calendar-swatch{width:26px;height:26px;border-radius:9px;display:grid;place-items:center;font-family:var(--rk-font-display);font-size:13px;font-weight:800;line-height:1;flex:none;box-shadow:var(--rk-shadow-xs)}
|
|
222
|
-
.rk-calendar-swatch-tiktok{background:var(--rk-ink);color:#fff}
|
|
223
|
-
.rk-calendar-swatch-instagram{background:linear-gradient(135deg,#f9ce34,#ee2a7b 55%,#6228d7);color:#fff}
|
|
224
|
-
.rk-calendar-swatch-youtube{background:#ff3b30;color:#fff}
|
|
225
|
-
.rk-calendar-swatch-email{background:var(--rk-sky-tint);color:#1d6fb8}
|
|
249
|
+
.rk-calendar-chip-time{flex:none;font-size:10px;font-weight:700;color:var(--rk-text-muted);font-variant-numeric:tabular-nums}
|
|
250
|
+
.rk-calendar-chip-title{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--rk-ink);letter-spacing:-.01em;
|
|
251
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
252
|
+
.rk-calendar-more{font-size:10px;font-weight:700;color:var(--rk-text-muted);padding:0 2px;background:none;border:0;cursor:default}
|
|
226
253
|
|
|
227
|
-
/* status dot —
|
|
228
|
-
.rk-calendar-dot{width:
|
|
254
|
+
/* status dot — chips + legend */
|
|
255
|
+
.rk-calendar-dot{width:7px;height:7px;border-radius:var(--rk-r-full);background:var(--rk-n-400);flex:none}
|
|
229
256
|
.rk-calendar-dot[data-status="review"]{background:var(--rk-gold-700)}
|
|
230
257
|
.rk-calendar-dot[data-status="ready"]{background:var(--rk-green)}
|
|
231
258
|
.rk-calendar-dot[data-status="scheduled"]{background:var(--rk-sky)}
|
|
232
259
|
.rk-calendar-dot[data-status="sent"]{background:var(--rk-violet)}
|
|
233
260
|
.rk-calendar-dot[data-status="error"]{background:var(--rk-red)}
|
|
234
261
|
|
|
235
|
-
/* compact one-line legend */
|
|
236
|
-
.rk-calendar-legend{display:flex;flex-wrap:wrap;gap:18px;margin-top:18px;padding:0 6px}
|
|
237
|
-
.rk-calendar-legend-item{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:600;color:var(--rk-text-muted)}
|
|
238
|
-
|
|
239
262
|
@media(max-width:760px){
|
|
240
263
|
.rk-calendar-weekdays,.rk-calendar-grid{min-width:660px}
|
|
241
|
-
.rk-calendar-
|
|
264
|
+
.rk-calendar-grid-card{overflow-x:auto}
|
|
265
|
+
.rk-calendar-toolbar{gap:10px}
|
|
242
266
|
}
|
|
243
267
|
`;
|
|
268
|
+
const script = `
|
|
269
|
+
(function(){
|
|
270
|
+
var root=document;
|
|
271
|
+
var bootEl=root.querySelector('[data-rk-cal-boot]');
|
|
272
|
+
var boot={};
|
|
273
|
+
try{ boot=JSON.parse(bootEl&&bootEl.textContent||'{}'); }catch(e){ boot={}; }
|
|
274
|
+
var loadEndpoint=boot.loadEndpoint||'/calendar/feed';
|
|
275
|
+
var gridEl=root.querySelector('[data-rk-cal-grid]');
|
|
276
|
+
var monthEl=root.querySelector('[data-rk-cal-month]');
|
|
277
|
+
var subEl=root.querySelector('[data-rk-cal-sub]');
|
|
278
|
+
var syncEl=root.querySelector('[data-rk-cal-sync]');
|
|
279
|
+
var noticeEl=root.querySelector('[data-rk-cal-notice]');
|
|
280
|
+
var channelsEl=root.querySelector('[data-rk-cal-channels]');
|
|
281
|
+
var chSearchEl=root.querySelector('[data-rk-cal-chsearch]');
|
|
282
|
+
if(!gridEl){return;}
|
|
283
|
+
|
|
284
|
+
var STATUS=${JSON.stringify(STATUS)};
|
|
285
|
+
var monthFmt=new Intl.DateTimeFormat(undefined,{month:'long',year:'numeric'});
|
|
286
|
+
var timeFmt=new Intl.DateTimeFormat(undefined,{hour:'numeric',minute:'2-digit'});
|
|
287
|
+
|
|
288
|
+
var today=startOfDay(new Date());
|
|
289
|
+
var cursor=startOfMonth(today);
|
|
290
|
+
// channelId -> channel record (resolved from the feed's channels list, seeded first)
|
|
291
|
+
var channelsById={};
|
|
292
|
+
var channelOrder=[];
|
|
293
|
+
ingestChannels(boot.seedChannels||[]);
|
|
294
|
+
// dateKey -> [posts]; seed with any server-provided posts
|
|
295
|
+
var postsByDay={};
|
|
296
|
+
ingestPosts(boot.seedPosts||[]);
|
|
297
|
+
var loading=false;
|
|
298
|
+
var selectedChannelId='';
|
|
299
|
+
var channelQuery='';
|
|
300
|
+
|
|
301
|
+
function esc(s){var d=root.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
|
|
302
|
+
function startOfDay(d){return new Date(d.getFullYear(),d.getMonth(),d.getDate());}
|
|
303
|
+
function startOfMonth(d){return new Date(d.getFullYear(),d.getMonth(),1);}
|
|
304
|
+
function startOfWeek(d){var n=startOfDay(d);n.setDate(n.getDate()-n.getDay());return n;}
|
|
305
|
+
function addDays(d,n){var x=new Date(d);x.setDate(x.getDate()+n);return startOfDay(x);}
|
|
306
|
+
function addMonths(d,n){var x=new Date(d.getFullYear(),d.getMonth()+n,1);return x;}
|
|
307
|
+
function toKey(d){return [d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-');}
|
|
308
|
+
function sameDay(a,b){return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth()&&a.getDate()===b.getDate();}
|
|
309
|
+
function sameMonth(a,b){return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth();}
|
|
310
|
+
|
|
311
|
+
function platformMeta(platform,channelId,kind){
|
|
312
|
+
var p=String(platform||'').toLowerCase();
|
|
313
|
+
if(p.indexOf('tiktok')>-1)return {initial:'T',swatch:'tiktok'};
|
|
314
|
+
if(p.indexOf('instagram')>-1)return {initial:'I',swatch:'instagram'};
|
|
315
|
+
if(p.indexOf('youtube')>-1)return {initial:'Y',swatch:'youtube'};
|
|
316
|
+
if(p.indexOf('email')>-1||/^email:/.test(String(channelId||''))||/email/i.test(String(kind||'')))return {initial:'E',swatch:'email'};
|
|
317
|
+
if(p.indexOf('facebook')>-1)return {initial:'F',swatch:'generic'};
|
|
318
|
+
if(p.indexOf('linkedin')>-1)return {initial:'L',swatch:'generic'};
|
|
319
|
+
if(p.indexOf('twitter')>-1||p==='x')return {initial:'X',swatch:'generic'};
|
|
320
|
+
var src=String(platform||channelId||'').replace(/^email:/,'').trim();
|
|
321
|
+
return {initial:(src.charAt(0)||'C').toUpperCase(),swatch:'generic'};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function ingestChannels(list){
|
|
325
|
+
(Array.isArray(list)?list:[]).forEach(function(ch){
|
|
326
|
+
if(!ch||ch.id==null)return;
|
|
327
|
+
var id=String(ch.id);
|
|
328
|
+
if(!channelsById[id])channelOrder.push(id);
|
|
329
|
+
channelsById[id]={
|
|
330
|
+
id:id,
|
|
331
|
+
name:ch.name||ch.title||id,
|
|
332
|
+
handle:ch.handle||null,
|
|
333
|
+
platform:ch.platform||null,
|
|
334
|
+
avatarUrl:ch.avatarUrl||null
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function ingestPosts(list){
|
|
340
|
+
(Array.isArray(list)?list:[]).forEach(function(post){
|
|
341
|
+
if(!post)return;
|
|
342
|
+
var t=Date.parse(post.scheduledAt);
|
|
343
|
+
if(isNaN(t))return;
|
|
344
|
+
var d=startOfDay(new Date(t));
|
|
345
|
+
var key=toKey(d);
|
|
346
|
+
if(!postsByDay[key])postsByDay[key]=[];
|
|
347
|
+
// de-dupe by id within the day
|
|
348
|
+
var id=String(post.id||post.scheduleId||'');
|
|
349
|
+
if(id&&postsByDay[key].some(function(p){return String(p.id||p.scheduleId||'')===id;}))return;
|
|
350
|
+
postsByDay[key].push(post);
|
|
351
|
+
postsByDay[key].sort(function(a,b){return Date.parse(a.scheduledAt)-Date.parse(b.scheduledAt);});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function postCountFor(channelId){
|
|
356
|
+
var n=0;
|
|
357
|
+
Object.keys(postsByDay).forEach(function(key){
|
|
358
|
+
postsByDay[key].forEach(function(p){ if(String(p.channelId)===channelId)n++; });
|
|
359
|
+
});
|
|
360
|
+
return n;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function channelInitials(name){
|
|
364
|
+
return String(name||'').split(/\\s+/).filter(Boolean).slice(0,2)
|
|
365
|
+
.map(function(part){return (part[0]||'').toUpperCase();}).join('')||'CH';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderChannels(){
|
|
369
|
+
if(!channelsEl)return;
|
|
370
|
+
var q=channelQuery.trim().toLowerCase();
|
|
371
|
+
var ids=channelOrder.filter(function(id){
|
|
372
|
+
if(!q)return true;
|
|
373
|
+
var ch=channelsById[id];
|
|
374
|
+
return [ch.name,ch.handle,ch.platform,ch.id].filter(Boolean).join(' ').toLowerCase().indexOf(q)>-1;
|
|
375
|
+
});
|
|
376
|
+
if(!channelOrder.length){
|
|
377
|
+
channelsEl.innerHTML='<div class="rk-calendar-channels-empty">No channels connected yet. Connect FlockPoster or add an email channel to start scheduling.</div>';
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if(!ids.length){
|
|
381
|
+
channelsEl.innerHTML='<div class="rk-calendar-channels-empty">No channels match \\u201c'+esc(channelQuery)+'\\u201d.</div>';
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
var allBtn='<button type="button" class="rk-calendar-channel'+(selectedChannelId===''?' is-selected':'')+'" data-rk-channel="">'
|
|
385
|
+
+'<span class="rk-calendar-channel-avatar"><span class="rk-calendar-channel-initials">All</span></span>'
|
|
386
|
+
+'<span class="rk-calendar-channel-main">'
|
|
387
|
+
+'<span class="rk-calendar-channel-name">All channels</span>'
|
|
388
|
+
+'<span class="rk-calendar-channel-sub">'+channelOrder.length+' connected</span>'
|
|
389
|
+
+'</span>'
|
|
390
|
+
+'</button>';
|
|
391
|
+
var items=ids.map(function(id){
|
|
392
|
+
var ch=channelsById[id];
|
|
393
|
+
var pm=platformMeta(ch.platform,ch.id,null);
|
|
394
|
+
var avatar=ch.avatarUrl
|
|
395
|
+
? '<img src="'+esc(ch.avatarUrl)+'" alt="" loading="lazy" />'
|
|
396
|
+
: '<span class="rk-calendar-channel-initials">'+esc(channelInitials(ch.name))+'</span>';
|
|
397
|
+
var count=postCountFor(id);
|
|
398
|
+
return '<button type="button" class="rk-calendar-channel'+(selectedChannelId===id?' is-selected':'')+'" data-rk-channel="'+esc(id)+'">'
|
|
399
|
+
+'<span class="rk-calendar-channel-avatar">'+avatar
|
|
400
|
+
+'<span class="rk-calendar-swatch rk-calendar-swatch-'+pm.swatch+'" aria-hidden="true">'+esc(pm.initial)+'</span>'
|
|
401
|
+
+'</span>'
|
|
402
|
+
+'<span class="rk-calendar-channel-main">'
|
|
403
|
+
+'<span class="rk-calendar-channel-name">'+esc(ch.name)+'</span>'
|
|
404
|
+
+'<span class="rk-calendar-channel-sub">'+esc([ch.platform,ch.handle].filter(Boolean).join(' \\u00b7 ')||'Channel')+'</span>'
|
|
405
|
+
+'</span>'
|
|
406
|
+
+(count?'<span class="rk-calendar-channel-count">'+count+'</span>':'')
|
|
407
|
+
+'</button>';
|
|
408
|
+
}).join('');
|
|
409
|
+
channelsEl.innerHTML=allBtn+items;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function chip(post){
|
|
413
|
+
var tone=String(post.statusTone||'draft');
|
|
414
|
+
var meta=STATUS[tone]||{label:tone,pill:'rk-pill'};
|
|
415
|
+
var time='';
|
|
416
|
+
var t=Date.parse(post.scheduledAt);
|
|
417
|
+
if(!isNaN(t))time=timeFmt.format(new Date(t));
|
|
418
|
+
var label=post.status?String(post.status):meta.label;
|
|
419
|
+
var title=post.title||'Scheduled post';
|
|
420
|
+
var ch=channelsById[String(post.channelId)];
|
|
421
|
+
var chName=ch?ch.name:'';
|
|
422
|
+
var tag='a';var attrs='';
|
|
423
|
+
if(post.url){attrs=' href="'+esc(post.url)+'" target="_blank" rel="noreferrer"';}else{tag='button';attrs=' type="button"';}
|
|
424
|
+
return '<'+tag+' class="rk-calendar-chip"'+attrs+' title="'+esc(title+' \\u00b7 '+label+(time?' \\u00b7 '+time:'')+(chName?' \\u00b7 '+chName:''))+'">'
|
|
425
|
+
+'<span class="rk-calendar-dot" data-status="'+esc(tone)+'" aria-hidden="true"></span>'
|
|
426
|
+
+(time?'<span class="rk-calendar-chip-time">'+esc(time)+'</span>':'')
|
|
427
|
+
+'<span class="rk-calendar-chip-title">'+esc(title)+'</span>'
|
|
428
|
+
+'</'+tag+'>';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function visiblePosts(key){
|
|
432
|
+
var dayPosts=postsByDay[key]||[];
|
|
433
|
+
if(!selectedChannelId)return dayPosts;
|
|
434
|
+
return dayPosts.filter(function(p){return String(p.channelId)===selectedChannelId;});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderGrid(){
|
|
438
|
+
monthEl.textContent=monthFmt.format(cursor);
|
|
439
|
+
var firstCell=startOfWeek(startOfMonth(cursor));
|
|
440
|
+
var lastOfMonth=new Date(cursor.getFullYear(),cursor.getMonth()+1,0);
|
|
441
|
+
var rows=Math.ceil((addDays(lastOfMonth,0).getTime()-firstCell.getTime())/(7*86400000))||5;
|
|
442
|
+
rows=Math.max(rows,Math.ceil(((startOfMonth(cursor).getDay())+lastOfMonth.getDate())/7));
|
|
443
|
+
var total=rows*7;
|
|
444
|
+
var cells=[];
|
|
445
|
+
var monthPostCount=0;
|
|
446
|
+
for(var i=0;i<total;i++){
|
|
447
|
+
var d=addDays(firstCell,i);
|
|
448
|
+
var key=toKey(d);
|
|
449
|
+
var dayPosts=visiblePosts(key);
|
|
450
|
+
var inMonth=sameMonth(d,cursor);
|
|
451
|
+
if(inMonth)monthPostCount+=dayPosts.length;
|
|
452
|
+
var isToday=sameDay(d,today);
|
|
453
|
+
var cls='rk-calendar-cell';
|
|
454
|
+
if(!inMonth)cls+=' is-out';
|
|
455
|
+
if(isToday)cls+=' is-today';
|
|
456
|
+
var shown=dayPosts.slice(0,2);
|
|
457
|
+
var overflow=dayPosts.length-shown.length;
|
|
458
|
+
var chips=shown.map(chip).join('');
|
|
459
|
+
var more=overflow>0?'<span class="rk-calendar-more">+'+overflow+' more</span>':'';
|
|
460
|
+
cells.push('<div class="'+cls+'">'
|
|
461
|
+
+'<div class="rk-calendar-cell-head"><span class="rk-calendar-daynum">'+d.getDate()+'</span></div>'
|
|
462
|
+
+'<div class="rk-calendar-cell-posts">'+chips+more+'</div>'
|
|
463
|
+
+'</div>');
|
|
464
|
+
}
|
|
465
|
+
gridEl.innerHTML=cells.join('');
|
|
466
|
+
if(subEl){
|
|
467
|
+
var scope=selectedChannelId&&channelsById[selectedChannelId]?(' on '+channelsById[selectedChannelId].name):'';
|
|
468
|
+
subEl.textContent=monthPostCount
|
|
469
|
+
?(monthPostCount+' scheduled post'+(monthPostCount===1?'':'s')+scope+' in '+monthFmt.format(cursor)+'.')
|
|
470
|
+
:('Nothing scheduled'+scope+' in '+monthFmt.format(cursor)+' yet.');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function setNotice(msg,isError){
|
|
475
|
+
if(!noticeEl)return;
|
|
476
|
+
if(!msg){noticeEl.hidden=true;noticeEl.textContent='';noticeEl.className='rk-calendar-notice';return;}
|
|
477
|
+
noticeEl.hidden=false;
|
|
478
|
+
noticeEl.className='rk-calendar-notice rk-notice '+(isError?'rk-notice-err':'rk-notice-ok');
|
|
479
|
+
noticeEl.textContent=msg;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function setSync(msg,isError){
|
|
483
|
+
if(!syncEl)return;
|
|
484
|
+
syncEl.textContent=msg||'';
|
|
485
|
+
syncEl.className='rk-calendar-sync'+(isError?' is-error':'');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function load(){
|
|
489
|
+
if(loading)return;
|
|
490
|
+
loading=true;
|
|
491
|
+
gridEl.classList.add('is-loading');
|
|
492
|
+
setSync('Loading\\u2026',false);
|
|
493
|
+
var first=startOfWeek(startOfMonth(cursor));
|
|
494
|
+
var last=addDays(first,41);
|
|
495
|
+
var params=new URLSearchParams({start_date:toKey(first),end_date:toKey(last)});
|
|
496
|
+
var url;
|
|
497
|
+
try{
|
|
498
|
+
var u=new URL(loadEndpoint,window.location.origin);
|
|
499
|
+
params.forEach(function(v,k){u.searchParams.set(k,v);});
|
|
500
|
+
url=u.pathname+u.search;
|
|
501
|
+
}catch(e){ url=loadEndpoint+(loadEndpoint.indexOf('?')>-1?'&':'?')+params.toString(); }
|
|
502
|
+
fetch(url,{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
503
|
+
.then(function(r){return r.json().catch(function(){return {};}).then(function(j){return {ok:r.ok,json:j};});})
|
|
504
|
+
.then(function(res){
|
|
505
|
+
var payload=res.json||{};
|
|
506
|
+
if(!res.ok){
|
|
507
|
+
var em=(payload&&typeof payload.error==='string'&&payload.error)?payload.error:'Unable to load calendar.';
|
|
508
|
+
throw new Error(em);
|
|
509
|
+
}
|
|
510
|
+
ingestChannels(payload.channels||[]);
|
|
511
|
+
ingestPosts(payload.posts||[]);
|
|
512
|
+
if(payload&&typeof payload.error==='string'&&payload.error){
|
|
513
|
+
setNotice(payload.error,false);
|
|
514
|
+
}else{
|
|
515
|
+
setNotice('',false);
|
|
516
|
+
}
|
|
517
|
+
setSync(payload&&payload.syncStatus==='error'?'Synced with warnings':'Up to date',payload&&payload.syncStatus==='error');
|
|
518
|
+
renderChannels();
|
|
519
|
+
renderGrid();
|
|
520
|
+
})
|
|
521
|
+
.catch(function(err){
|
|
522
|
+
var msg=(err&&err.message)?err.message:'Unable to load calendar.';
|
|
523
|
+
setNotice(msg,true);
|
|
524
|
+
setSync('Offline',true);
|
|
525
|
+
renderChannels();
|
|
526
|
+
renderGrid();
|
|
527
|
+
})
|
|
528
|
+
.finally(function(){
|
|
529
|
+
loading=false;
|
|
530
|
+
gridEl.classList.remove('is-loading');
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if(channelsEl){
|
|
535
|
+
channelsEl.addEventListener('click',function(e){
|
|
536
|
+
var btn=e.target.closest('[data-rk-channel]');
|
|
537
|
+
if(!btn)return;
|
|
538
|
+
selectedChannelId=btn.getAttribute('data-rk-channel')||'';
|
|
539
|
+
renderChannels();
|
|
540
|
+
renderGrid();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
if(chSearchEl){
|
|
544
|
+
chSearchEl.addEventListener('input',function(){
|
|
545
|
+
channelQuery=chSearchEl.value||'';
|
|
546
|
+
renderChannels();
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
root.querySelector('[data-rk-cal-prev]')&&root.querySelector('[data-rk-cal-prev]').addEventListener('click',function(){cursor=addMonths(cursor,-1);renderGrid();load();});
|
|
551
|
+
root.querySelector('[data-rk-cal-next]')&&root.querySelector('[data-rk-cal-next]').addEventListener('click',function(){cursor=addMonths(cursor,1);renderGrid();load();});
|
|
552
|
+
root.querySelector('[data-rk-cal-today]')&&root.querySelector('[data-rk-cal-today]').addEventListener('click',function(){cursor=startOfMonth(today);renderGrid();load();});
|
|
553
|
+
|
|
554
|
+
renderChannels();
|
|
555
|
+
renderGrid();
|
|
556
|
+
load();
|
|
557
|
+
})();`;
|
|
244
558
|
return reskinDocument({
|
|
245
559
|
title: "vidfarm reskin — Calendar",
|
|
246
|
-
description: "Reskinned vidfarm calendar —
|
|
560
|
+
description: "Reskinned vidfarm calendar — connected channels on the left, a compact viewport-fit month of scheduled posts on the right, live from /calendar/feed.",
|
|
247
561
|
activeSlug: "calendar",
|
|
248
562
|
pageCss,
|
|
249
|
-
body
|
|
563
|
+
body,
|
|
564
|
+
script
|
|
250
565
|
});
|
|
251
566
|
}
|
|
252
567
|
//# sourceMappingURL=calendar-page.js.map
|