@fayz-ai/plugin-marketing 0.8.0 → 0.8.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/dist/MarketingContext.d.ts +2 -0
- package/dist/MarketingContext.d.ts.map +1 -1
- package/dist/MarketingPage.d.ts.map +1 -1
- package/dist/data/blogEntities.d.ts +28 -0
- package/dist/data/blogEntities.d.ts.map +1 -0
- package/dist/index.cjs +509 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +515 -49
- package/dist/index.js.map +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
- package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
- package/dist/integrations/ranklayer/types.d.ts +16 -0
- package/dist/integrations/ranklayer/types.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/views/blog/BlogAdminView.d.ts +5 -0
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
- package/dist/views/blog/BlogEntityForm.d.ts +11 -0
- package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
- package/dist/views/blog/useEntityCrud.d.ts +29 -0
- package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/MarketingContext.tsx +2 -0
- package/src/MarketingPage.tsx +22 -1
- package/src/data/blogEntities.tsx +91 -0
- package/src/index.ts +16 -1
- package/src/integrations/ranklayer/RANKLAYER.md +40 -0
- package/src/integrations/ranklayer/connectorDef.tsx +98 -0
- package/src/integrations/ranklayer/data/supabase.ts +106 -0
- package/src/integrations/ranklayer/types.ts +16 -0
- package/src/migrations/004_ranklayer.sql +50 -0
- package/src/migrations/index.ts +54 -1
- package/src/presets.ts +5 -3
- package/src/views/blog/BlogAdminView.tsx +99 -0
- package/src/views/blog/BlogEntityForm.tsx +92 -0
- package/src/views/blog/useEntityCrud.ts +50 -0
package/dist/index.cjs
CHANGED
|
@@ -219,11 +219,11 @@ function ChannelDetailView({ channelId, onBack }) {
|
|
|
219
219
|
const range = useMarketingStore((s) => s.range);
|
|
220
220
|
const channels = useMarketingStore((s) => s.channels);
|
|
221
221
|
const lookup = useChannelLookup();
|
|
222
|
-
const
|
|
222
|
+
const provider2 = useMarketingProvider();
|
|
223
223
|
const [campaigns, setCampaigns] = React8__default.default.useState([]);
|
|
224
224
|
React8__default.default.useEffect(() => {
|
|
225
|
-
void
|
|
226
|
-
}, [
|
|
225
|
+
void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
|
|
226
|
+
}, [provider2, range, channelId]);
|
|
227
227
|
const perf = channels.find((c) => c.channelId === channelId);
|
|
228
228
|
const channel = lookup.get(channelId);
|
|
229
229
|
const columns = React8__default.default.useMemo(() => [
|
|
@@ -1968,7 +1968,7 @@ function toDraft2(post) {
|
|
|
1968
1968
|
}
|
|
1969
1969
|
function PostPage({ postId, onBack }) {
|
|
1970
1970
|
const t = core.useTranslation();
|
|
1971
|
-
const
|
|
1971
|
+
const provider2 = useContentPlannerProvider();
|
|
1972
1972
|
const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
|
|
1973
1973
|
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1974
1974
|
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
@@ -1985,13 +1985,13 @@ function PostPage({ postId, onBack }) {
|
|
|
1985
1985
|
setDraft(toDraft2(storePost));
|
|
1986
1986
|
return;
|
|
1987
1987
|
}
|
|
1988
|
-
void
|
|
1988
|
+
void provider2.getPost(postId).then((fetched) => {
|
|
1989
1989
|
if (fetched) {
|
|
1990
1990
|
setPost(fetched);
|
|
1991
1991
|
setDraft(toDraft2(fetched));
|
|
1992
1992
|
}
|
|
1993
1993
|
});
|
|
1994
|
-
}, [postId, storePost, post?.id,
|
|
1994
|
+
}, [postId, storePost, post?.id, provider2]);
|
|
1995
1995
|
if (!post || !draft) {
|
|
1996
1996
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
1997
1997
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
@@ -2178,6 +2178,239 @@ function PostPage({ postId, onBack }) {
|
|
|
2178
2178
|
post.id
|
|
2179
2179
|
);
|
|
2180
2180
|
}
|
|
2181
|
+
|
|
2182
|
+
// src/data/blogEntities.tsx
|
|
2183
|
+
var BLOG_POSTS_TABLE = "plg_blog_posts";
|
|
2184
|
+
var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
|
|
2185
|
+
function buildBlogPostEntity() {
|
|
2186
|
+
const fields = [
|
|
2187
|
+
{ key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2188
|
+
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
|
|
2189
|
+
{ key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
|
|
2190
|
+
{ key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
|
|
2191
|
+
{ key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
|
|
2192
|
+
{ key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
|
|
2193
|
+
{ key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2194
|
+
{ key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
|
|
2195
|
+
{ key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
|
|
2196
|
+
{ key: "authorName", label: "Author name", type: "text", showInTable: false },
|
|
2197
|
+
{ key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
|
|
2198
|
+
{ key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2199
|
+
{ key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
|
|
2200
|
+
];
|
|
2201
|
+
return {
|
|
2202
|
+
name: "Post",
|
|
2203
|
+
namePlural: "Posts",
|
|
2204
|
+
icon: "Newspaper",
|
|
2205
|
+
fields,
|
|
2206
|
+
displayField: "title",
|
|
2207
|
+
subtitleField: "slug",
|
|
2208
|
+
facets: [{ field: "status", allLabel: "All" }],
|
|
2209
|
+
data: {
|
|
2210
|
+
table: BLOG_POSTS_TABLE,
|
|
2211
|
+
tenantScoped: true,
|
|
2212
|
+
searchColumns: ["title", "slug", "excerpt"]
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function buildBlogCategoryEntity() {
|
|
2217
|
+
const fields = [
|
|
2218
|
+
{ key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2219
|
+
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
|
|
2220
|
+
{ key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
|
|
2221
|
+
];
|
|
2222
|
+
return {
|
|
2223
|
+
name: "Category",
|
|
2224
|
+
namePlural: "Categories",
|
|
2225
|
+
icon: "Tags",
|
|
2226
|
+
fields,
|
|
2227
|
+
displayField: "name",
|
|
2228
|
+
data: {
|
|
2229
|
+
table: BLOG_CATEGORIES_TABLE,
|
|
2230
|
+
tenantScoped: true,
|
|
2231
|
+
searchColumns: ["name", "slug"]
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function useEntityCrud(entity) {
|
|
2236
|
+
const provider2 = React8.useMemo(() => core.resolveDataProvider(entity), [entity]);
|
|
2237
|
+
const [items, setItems] = React8.useState(null);
|
|
2238
|
+
const [total, setTotal] = React8.useState(0);
|
|
2239
|
+
const fetch = React8.useCallback(async (query = {}) => {
|
|
2240
|
+
try {
|
|
2241
|
+
const res = await provider2.list(query);
|
|
2242
|
+
setItems(res.data);
|
|
2243
|
+
setTotal(res.total);
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
|
|
2246
|
+
setItems([]);
|
|
2247
|
+
setTotal(0);
|
|
2248
|
+
}
|
|
2249
|
+
}, [provider2]);
|
|
2250
|
+
const getById = React8.useCallback(async (id) => {
|
|
2251
|
+
const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
|
|
2252
|
+
return res.data[0] ?? null;
|
|
2253
|
+
}, [provider2]);
|
|
2254
|
+
const create = React8.useCallback((data) => provider2.create(data), [provider2]);
|
|
2255
|
+
const update = React8.useCallback((id, data) => provider2.update(id, data), [provider2]);
|
|
2256
|
+
const remove = React8.useCallback((id) => provider2.remove(id), [provider2]);
|
|
2257
|
+
return { items, total, fetch, getById, create, update, remove };
|
|
2258
|
+
}
|
|
2259
|
+
function PostsList({ navigate }) {
|
|
2260
|
+
const entity = React8.useMemo(() => buildBlogPostEntity(), []);
|
|
2261
|
+
const { items, total, fetch } = useEntityCrud(entity);
|
|
2262
|
+
const [search, setSearch] = React8.useState("");
|
|
2263
|
+
const [statusFilter, setStatusFilter] = React8.useState();
|
|
2264
|
+
const [loadedOnce, setLoadedOnce] = React8.useState(false);
|
|
2265
|
+
React8.useEffect(() => {
|
|
2266
|
+
let active = true;
|
|
2267
|
+
Promise.resolve(
|
|
2268
|
+
fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
|
|
2269
|
+
).finally(() => {
|
|
2270
|
+
if (active) setLoadedOnce(true);
|
|
2271
|
+
});
|
|
2272
|
+
return () => {
|
|
2273
|
+
active = false;
|
|
2274
|
+
};
|
|
2275
|
+
}, [search, statusFilter, fetch]);
|
|
2276
|
+
const facets = React8.useMemo(() => [{
|
|
2277
|
+
field: "status",
|
|
2278
|
+
allLabel: "All",
|
|
2279
|
+
options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
|
|
2280
|
+
}], []);
|
|
2281
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2282
|
+
saas.CrudListView,
|
|
2283
|
+
{
|
|
2284
|
+
entityDef: entity,
|
|
2285
|
+
items: loadedOnce ? items : null,
|
|
2286
|
+
total,
|
|
2287
|
+
search,
|
|
2288
|
+
onSearchChange: setSearch,
|
|
2289
|
+
searchPlaceholder: "Search posts\u2026",
|
|
2290
|
+
facets,
|
|
2291
|
+
activeFilters: { status: statusFilter },
|
|
2292
|
+
onFacetChange: (_field, value) => setStatusFilter(value),
|
|
2293
|
+
onNew: () => navigate("blog-new"),
|
|
2294
|
+
addLabel: "New post",
|
|
2295
|
+
onRowClick: (row) => navigate(`blog-post:${row.id}`)
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
function CategoriesList({ navigate }) {
|
|
2300
|
+
const entity = React8.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2301
|
+
const { items, total, fetch } = useEntityCrud(entity);
|
|
2302
|
+
const [search, setSearch] = React8.useState("");
|
|
2303
|
+
const [loadedOnce, setLoadedOnce] = React8.useState(false);
|
|
2304
|
+
React8.useEffect(() => {
|
|
2305
|
+
let active = true;
|
|
2306
|
+
Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
|
|
2307
|
+
if (active) setLoadedOnce(true);
|
|
2308
|
+
});
|
|
2309
|
+
return () => {
|
|
2310
|
+
active = false;
|
|
2311
|
+
};
|
|
2312
|
+
}, [search, fetch]);
|
|
2313
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2314
|
+
saas.CrudListView,
|
|
2315
|
+
{
|
|
2316
|
+
entityDef: entity,
|
|
2317
|
+
items: loadedOnce ? items : null,
|
|
2318
|
+
total,
|
|
2319
|
+
search,
|
|
2320
|
+
onSearchChange: setSearch,
|
|
2321
|
+
searchPlaceholder: "Search categories\u2026",
|
|
2322
|
+
onNew: () => navigate("blog-cat-new"),
|
|
2323
|
+
addLabel: "New category",
|
|
2324
|
+
onRowClick: (row) => navigate(`blog-cat:${row.id}`)
|
|
2325
|
+
}
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
function BlogAdminView({ navigate }) {
|
|
2329
|
+
const { labels } = useMarketingConfig();
|
|
2330
|
+
const [lens, setLens] = React8.useState("posts");
|
|
2331
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2332
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2333
|
+
ui.SegmentedControl,
|
|
2334
|
+
{
|
|
2335
|
+
"aria-label": "Blog section",
|
|
2336
|
+
options: [
|
|
2337
|
+
{ value: "posts", label: labels.blog },
|
|
2338
|
+
{ value: "categories", label: labels.blogCategories }
|
|
2339
|
+
],
|
|
2340
|
+
value: lens,
|
|
2341
|
+
onChange: (v) => setLens(v)
|
|
2342
|
+
}
|
|
2343
|
+
),
|
|
2344
|
+
lens === "posts" ? /* @__PURE__ */ jsxRuntime.jsx(PostsList, { navigate }) : /* @__PURE__ */ jsxRuntime.jsx(CategoriesList, { navigate })
|
|
2345
|
+
] });
|
|
2346
|
+
}
|
|
2347
|
+
function BlogEntityForm({
|
|
2348
|
+
entity,
|
|
2349
|
+
editId,
|
|
2350
|
+
parentLabel,
|
|
2351
|
+
onDone
|
|
2352
|
+
}) {
|
|
2353
|
+
const { getById, create, update, remove } = useEntityCrud(entity);
|
|
2354
|
+
const isEdit = !!editId;
|
|
2355
|
+
const namePlural = entity.namePlural ?? entity.name + "s";
|
|
2356
|
+
const [initialData, setInitialData] = React8__default.default.useState(isEdit ? null : {});
|
|
2357
|
+
React8__default.default.useEffect(() => {
|
|
2358
|
+
if (!editId) return;
|
|
2359
|
+
let cancelled = false;
|
|
2360
|
+
(async () => {
|
|
2361
|
+
const item = await getById(editId);
|
|
2362
|
+
if (!cancelled) setInitialData(item ?? {});
|
|
2363
|
+
})();
|
|
2364
|
+
return () => {
|
|
2365
|
+
cancelled = true;
|
|
2366
|
+
};
|
|
2367
|
+
}, [editId, getById]);
|
|
2368
|
+
async function save(values) {
|
|
2369
|
+
try {
|
|
2370
|
+
if (isEdit) await update(editId, values);
|
|
2371
|
+
else await create(values);
|
|
2372
|
+
ui.toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
|
|
2373
|
+
onDone();
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
ui.toast.error(`Failed to save ${entity.name.toLowerCase()}`);
|
|
2376
|
+
throw err;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
async function del() {
|
|
2380
|
+
if (!editId) return;
|
|
2381
|
+
try {
|
|
2382
|
+
await remove(editId);
|
|
2383
|
+
ui.toast.success(`${entity.name} deleted`);
|
|
2384
|
+
onDone();
|
|
2385
|
+
} catch {
|
|
2386
|
+
ui.toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
|
|
2390
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2391
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2392
|
+
ui.SubpageHeader,
|
|
2393
|
+
{
|
|
2394
|
+
title: String(title),
|
|
2395
|
+
onBack: onDone,
|
|
2396
|
+
parentLabel,
|
|
2397
|
+
actions: isEdit ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
|
|
2398
|
+
}
|
|
2399
|
+
),
|
|
2400
|
+
initialData !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2401
|
+
saas.CrudFormPage,
|
|
2402
|
+
{
|
|
2403
|
+
entityDef: entity,
|
|
2404
|
+
mode: isEdit ? "edit" : "create",
|
|
2405
|
+
initialData,
|
|
2406
|
+
namePlural,
|
|
2407
|
+
onCancel: onDone,
|
|
2408
|
+
onSubmit: save,
|
|
2409
|
+
hideBreadcrumb: true
|
|
2410
|
+
}
|
|
2411
|
+
)
|
|
2412
|
+
] });
|
|
2413
|
+
}
|
|
2181
2414
|
function buildNav(config, view, navigate) {
|
|
2182
2415
|
const items = [
|
|
2183
2416
|
{ id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
|
|
@@ -2209,9 +2442,18 @@ function buildNav(config, view, navigate) {
|
|
|
2209
2442
|
onClick: () => navigate("content")
|
|
2210
2443
|
});
|
|
2211
2444
|
}
|
|
2445
|
+
if (config.modules.blog) {
|
|
2446
|
+
items.push({
|
|
2447
|
+
id: "blog",
|
|
2448
|
+
label: config.labels.blog,
|
|
2449
|
+
icon: "Newspaper",
|
|
2450
|
+
active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
|
|
2451
|
+
onClick: () => navigate("blog")
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2212
2454
|
return items;
|
|
2213
2455
|
}
|
|
2214
|
-
function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
|
|
2456
|
+
function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
|
|
2215
2457
|
const t = core.useTranslation();
|
|
2216
2458
|
const { view, direction, navigate } = saas.useModuleNavigation("/marketing", {
|
|
2217
2459
|
overview: 0,
|
|
@@ -2220,15 +2462,22 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
|
|
|
2220
2462
|
funnel: 0,
|
|
2221
2463
|
"landing-pages": 0,
|
|
2222
2464
|
content: 0,
|
|
2465
|
+
blog: 0,
|
|
2223
2466
|
"channel-detail": 1,
|
|
2224
2467
|
"content-post": 1,
|
|
2225
|
-
"content-brief": 1
|
|
2468
|
+
"content-brief": 1,
|
|
2469
|
+
"blog-new": 1,
|
|
2470
|
+
"blog-post": 1,
|
|
2471
|
+
"blog-cat-new": 1,
|
|
2472
|
+
"blog-cat": 1
|
|
2226
2473
|
}, "overview");
|
|
2227
2474
|
React8__default.default.useEffect(() => {
|
|
2228
2475
|
void store.getState().load();
|
|
2229
2476
|
}, [store]);
|
|
2230
2477
|
const isOverview = view === "overview";
|
|
2231
2478
|
const nav = buildNav(config, view, navigate);
|
|
2479
|
+
const blogPostEntity = React8__default.default.useMemo(() => buildBlogPostEntity(), []);
|
|
2480
|
+
const blogCategoryEntity = React8__default.default.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2232
2481
|
const quickActions = React8__default.default.useMemo(() => [
|
|
2233
2482
|
{ id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
|
|
2234
2483
|
], [t, config.labels.campaigns]);
|
|
@@ -2241,9 +2490,14 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
|
|
|
2241
2490
|
{ id: "content", render: () => /* @__PURE__ */ jsxRuntime.jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2242
2491
|
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2243
2492
|
{ id: "content-brief", render: () => /* @__PURE__ */ jsxRuntime.jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
2493
|
+
{ id: "blog", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogAdminView, { navigate }) },
|
|
2494
|
+
{ id: "blog-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2495
|
+
{ id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2496
|
+
{ id: "blog-cat-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2497
|
+
{ id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2244
2498
|
{ id: "overview", render: () => /* @__PURE__ */ jsxRuntime.jsx(OverviewView, {}) }
|
|
2245
2499
|
], "overview");
|
|
2246
|
-
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2500
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsxRuntime.jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2247
2501
|
ui.ModulePage,
|
|
2248
2502
|
{
|
|
2249
2503
|
title: config.labels.pageTitle,
|
|
@@ -2518,7 +2772,7 @@ var AGENCY = {
|
|
|
2518
2772
|
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
2519
2773
|
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
2520
2774
|
],
|
|
2521
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
|
|
2775
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2522
2776
|
};
|
|
2523
2777
|
var BEAUTY = {
|
|
2524
2778
|
conversion: {
|
|
@@ -2536,7 +2790,7 @@ var BEAUTY = {
|
|
|
2536
2790
|
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
2537
2791
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2538
2792
|
],
|
|
2539
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
|
|
2793
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
|
|
2540
2794
|
};
|
|
2541
2795
|
var RESTO = {
|
|
2542
2796
|
conversion: {
|
|
@@ -2554,7 +2808,7 @@ var RESTO = {
|
|
|
2554
2808
|
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
2555
2809
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2556
2810
|
],
|
|
2557
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
|
|
2811
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2558
2812
|
};
|
|
2559
2813
|
var MARKETING_PRESETS = {
|
|
2560
2814
|
agency: AGENCY,
|
|
@@ -3215,7 +3469,7 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3215
3469
|
}
|
|
3216
3470
|
};
|
|
3217
3471
|
}
|
|
3218
|
-
function createMarketingStore(
|
|
3472
|
+
function createMarketingStore(provider2) {
|
|
3219
3473
|
return vanilla.createStore((set, get) => ({
|
|
3220
3474
|
range: "30d",
|
|
3221
3475
|
overview: null,
|
|
@@ -3228,11 +3482,11 @@ function createMarketingStore(provider) {
|
|
|
3228
3482
|
const range = get().range;
|
|
3229
3483
|
set({ loading: true });
|
|
3230
3484
|
const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3485
|
+
provider2.overview({ range }),
|
|
3486
|
+
provider2.channelPerformance({ range }),
|
|
3487
|
+
provider2.listCampaigns({ range }),
|
|
3488
|
+
provider2.funnel({ range }),
|
|
3489
|
+
provider2.landingPages({ range })
|
|
3236
3490
|
]);
|
|
3237
3491
|
set({ overview, channels, campaigns, funnel, landingPages, loading: false });
|
|
3238
3492
|
},
|
|
@@ -3241,21 +3495,21 @@ function createMarketingStore(provider) {
|
|
|
3241
3495
|
await get().load();
|
|
3242
3496
|
},
|
|
3243
3497
|
async saveCampaign(input) {
|
|
3244
|
-
const created = await
|
|
3498
|
+
const created = await provider2.saveCampaign(input);
|
|
3245
3499
|
await get().load();
|
|
3246
3500
|
return created;
|
|
3247
3501
|
},
|
|
3248
3502
|
async deleteCampaign(id) {
|
|
3249
|
-
await
|
|
3503
|
+
await provider2.deleteCampaign(id);
|
|
3250
3504
|
await get().load();
|
|
3251
3505
|
}
|
|
3252
3506
|
}));
|
|
3253
3507
|
}
|
|
3254
|
-
function createContentPlannerStore(
|
|
3508
|
+
function createContentPlannerStore(provider2) {
|
|
3255
3509
|
async function loadPlansFor(accountId) {
|
|
3256
|
-
const plans = await
|
|
3510
|
+
const plans = await provider2.listPlans(accountId);
|
|
3257
3511
|
const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
|
|
3258
|
-
const posts = activePlan ? await
|
|
3512
|
+
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3259
3513
|
return { plans, activePlan, posts };
|
|
3260
3514
|
}
|
|
3261
3515
|
return vanilla.createStore((set, get) => ({
|
|
@@ -3275,7 +3529,7 @@ function createContentPlannerStore(provider) {
|
|
|
3275
3529
|
if (get().loading) return;
|
|
3276
3530
|
set({ loading: true });
|
|
3277
3531
|
try {
|
|
3278
|
-
const accounts = await
|
|
3532
|
+
const accounts = await provider2.listAccounts();
|
|
3279
3533
|
const current = get().activeAccountId;
|
|
3280
3534
|
const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
|
|
3281
3535
|
const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
|
|
@@ -3306,19 +3560,19 @@ function createContentPlannerStore(provider) {
|
|
|
3306
3560
|
},
|
|
3307
3561
|
async selectPlan(planId) {
|
|
3308
3562
|
const plan = get().plans.find((p) => p.id === planId) ?? null;
|
|
3309
|
-
const posts = plan ? await
|
|
3563
|
+
const posts = plan ? await provider2.listPosts(plan.id) : [];
|
|
3310
3564
|
set({ activePlan: plan, posts });
|
|
3311
3565
|
},
|
|
3312
3566
|
async saveAccount(input) {
|
|
3313
|
-
const account = await
|
|
3314
|
-
const accounts = await
|
|
3567
|
+
const account = await provider2.saveAccount(input);
|
|
3568
|
+
const accounts = await provider2.listAccounts();
|
|
3315
3569
|
set({ accounts });
|
|
3316
3570
|
if (!get().activeAccountId) await get().setAccount(account.id);
|
|
3317
3571
|
return account;
|
|
3318
3572
|
},
|
|
3319
3573
|
async deleteAccount(id) {
|
|
3320
|
-
await
|
|
3321
|
-
const accounts = await
|
|
3574
|
+
await provider2.deleteAccount(id);
|
|
3575
|
+
const accounts = await provider2.listAccounts();
|
|
3322
3576
|
const wasActive = get().activeAccountId === id;
|
|
3323
3577
|
set({ accounts });
|
|
3324
3578
|
if (wasActive) {
|
|
@@ -3331,30 +3585,30 @@ function createContentPlannerStore(provider) {
|
|
|
3331
3585
|
}
|
|
3332
3586
|
},
|
|
3333
3587
|
async savePlan(input) {
|
|
3334
|
-
const plan = await
|
|
3588
|
+
const plan = await provider2.savePlan(input);
|
|
3335
3589
|
const accountId = get().activeAccountId;
|
|
3336
3590
|
if (accountId) {
|
|
3337
|
-
const plans = await
|
|
3591
|
+
const plans = await provider2.listPlans(accountId);
|
|
3338
3592
|
const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
|
|
3339
|
-
const posts = activePlan ? await
|
|
3593
|
+
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3340
3594
|
set({ plans, activePlan, posts });
|
|
3341
3595
|
}
|
|
3342
3596
|
return plan;
|
|
3343
3597
|
},
|
|
3344
3598
|
async savePost(input) {
|
|
3345
|
-
const post = await
|
|
3599
|
+
const post = await provider2.savePost(input);
|
|
3346
3600
|
const planId = get().activePlan?.id;
|
|
3347
3601
|
if (planId && post.planId === planId) {
|
|
3348
|
-
const posts = await
|
|
3602
|
+
const posts = await provider2.listPosts(planId);
|
|
3349
3603
|
set({ posts });
|
|
3350
3604
|
}
|
|
3351
3605
|
return post;
|
|
3352
3606
|
},
|
|
3353
3607
|
async deletePost(id) {
|
|
3354
|
-
await
|
|
3608
|
+
await provider2.deletePost(id);
|
|
3355
3609
|
const planId = get().activePlan?.id;
|
|
3356
3610
|
if (planId) {
|
|
3357
|
-
const posts = await
|
|
3611
|
+
const posts = await provider2.listPosts(planId);
|
|
3358
3612
|
set({ posts });
|
|
3359
3613
|
}
|
|
3360
3614
|
}
|
|
@@ -3525,6 +3779,204 @@ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authen
|
|
|
3525
3779
|
CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3526
3780
|
CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3527
3781
|
`;
|
|
3782
|
+
var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
|
|
3783
|
+
-- Control-plane state for the RankLayer connector rendered in Marketing \u2192
|
|
3784
|
+
-- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
|
|
3785
|
+
-- (edge function + RankLayer API calls) lands via an external PR \u2014 see
|
|
3786
|
+
-- src/integrations/ranklayer/RANKLAYER.md.
|
|
3787
|
+
|
|
3788
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
|
|
3789
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3790
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3791
|
+
api_key text,
|
|
3792
|
+
site_domain text,
|
|
3793
|
+
active boolean NOT NULL DEFAULT true,
|
|
3794
|
+
last_sync_at timestamptz,
|
|
3795
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3796
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3797
|
+
);
|
|
3798
|
+
ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
|
|
3799
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
|
|
3800
|
+
ON public.plg_marketing_ranklayer_integrations(tenant_id);
|
|
3801
|
+
|
|
3802
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
|
|
3803
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3804
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3805
|
+
integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
|
|
3806
|
+
status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
|
|
3807
|
+
message text,
|
|
3808
|
+
fetched integer NOT NULL DEFAULT 0,
|
|
3809
|
+
written integer NOT NULL DEFAULT 0,
|
|
3810
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3811
|
+
);
|
|
3812
|
+
ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
|
|
3813
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
|
|
3814
|
+
ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
|
|
3815
|
+
|
|
3816
|
+
-- RLS: authenticated tenant CRUD
|
|
3817
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
|
|
3818
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
|
|
3819
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
|
|
3820
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
|
|
3821
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3822
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3823
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3824
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3825
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
|
|
3826
|
+
|
|
3827
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
|
|
3828
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
|
|
3829
|
+
CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3830
|
+
CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
3831
|
+
GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
|
|
3832
|
+
`;
|
|
3833
|
+
var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
|
|
3834
|
+
var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
|
|
3835
|
+
function getClient2() {
|
|
3836
|
+
const supabase = core.getSupabaseClientOptional();
|
|
3837
|
+
if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
|
|
3838
|
+
return supabase;
|
|
3839
|
+
}
|
|
3840
|
+
function requireTenantId2() {
|
|
3841
|
+
const tenantId = core.getActiveTenantId();
|
|
3842
|
+
if (!tenantId) throw new Error("[ranklayer] No active tenant");
|
|
3843
|
+
return tenantId;
|
|
3844
|
+
}
|
|
3845
|
+
function toIntegration(row) {
|
|
3846
|
+
return {
|
|
3847
|
+
id: row.id,
|
|
3848
|
+
apiKey: row.api_key ?? void 0,
|
|
3849
|
+
siteDomain: row.site_domain ?? void 0,
|
|
3850
|
+
active: row.active ?? true,
|
|
3851
|
+
lastSyncAt: row.last_sync_at ?? void 0
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function createRankLayerProvider() {
|
|
3855
|
+
return {
|
|
3856
|
+
async getIntegration() {
|
|
3857
|
+
const tenantId = core.getActiveTenantId();
|
|
3858
|
+
if (!tenantId) return null;
|
|
3859
|
+
const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
|
|
3860
|
+
return data ? toIntegration(data) : null;
|
|
3861
|
+
},
|
|
3862
|
+
async saveIntegration(values) {
|
|
3863
|
+
const tenantId = requireTenantId2();
|
|
3864
|
+
const payload = {
|
|
3865
|
+
tenant_id: tenantId,
|
|
3866
|
+
api_key: values.apiKey,
|
|
3867
|
+
site_domain: values.siteDomain ?? null,
|
|
3868
|
+
active: true,
|
|
3869
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3870
|
+
};
|
|
3871
|
+
const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
|
|
3872
|
+
if (error) throw error;
|
|
3873
|
+
return toIntegration(data);
|
|
3874
|
+
},
|
|
3875
|
+
async disconnect() {
|
|
3876
|
+
const tenantId = core.getActiveTenantId();
|
|
3877
|
+
if (!tenantId) return;
|
|
3878
|
+
await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
|
|
3879
|
+
},
|
|
3880
|
+
// SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
|
|
3881
|
+
// flow can be exercised. Replace with a real credential check in the PR.
|
|
3882
|
+
async testConnection(values) {
|
|
3883
|
+
if (!values.apiKey || values.apiKey.trim().length < 8) {
|
|
3884
|
+
return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
|
|
3885
|
+
}
|
|
3886
|
+
return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
|
|
3887
|
+
},
|
|
3888
|
+
async getSyncLog(integrationId) {
|
|
3889
|
+
const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
|
|
3890
|
+
return (data ?? []).map((r) => ({
|
|
3891
|
+
id: r.id,
|
|
3892
|
+
createdAt: r.created_at,
|
|
3893
|
+
status: r.status,
|
|
3894
|
+
message: r.message ?? void 0,
|
|
3895
|
+
fetched: r.fetched ?? 0,
|
|
3896
|
+
written: r.written ?? 0
|
|
3897
|
+
}));
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
var provider = createRankLayerProvider();
|
|
3902
|
+
function RankLayerExtraPanel() {
|
|
3903
|
+
const [integration, setIntegration] = React8.useState(null);
|
|
3904
|
+
const [log, setLog] = React8.useState([]);
|
|
3905
|
+
const [loaded, setLoaded] = React8.useState(false);
|
|
3906
|
+
React8.useEffect(() => {
|
|
3907
|
+
let cancelled = false;
|
|
3908
|
+
(async () => {
|
|
3909
|
+
try {
|
|
3910
|
+
const found = await provider.getIntegration();
|
|
3911
|
+
if (cancelled) return;
|
|
3912
|
+
setIntegration(found);
|
|
3913
|
+
if (found) setLog(await provider.getSyncLog(found.id));
|
|
3914
|
+
} catch {
|
|
3915
|
+
} finally {
|
|
3916
|
+
if (!cancelled) setLoaded(true);
|
|
3917
|
+
}
|
|
3918
|
+
})();
|
|
3919
|
+
return () => {
|
|
3920
|
+
cancelled = true;
|
|
3921
|
+
};
|
|
3922
|
+
}, []);
|
|
3923
|
+
if (!loaded || !integration) return null;
|
|
3924
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 border-t pt-4", children: [
|
|
3925
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
|
|
3926
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
3927
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
3928
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
|
|
3929
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
3930
|
+
"A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
|
|
3931
|
+
integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
|
|
3932
|
+
" Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
|
|
3933
|
+
] })
|
|
3934
|
+
] })
|
|
3935
|
+
] }),
|
|
3936
|
+
log.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
3937
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3938
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
3939
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
|
|
3940
|
+
] }),
|
|
3941
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
|
|
3942
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
|
|
3943
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto text-xs", children: [
|
|
3944
|
+
r.written,
|
|
3945
|
+
"/",
|
|
3946
|
+
r.fetched,
|
|
3947
|
+
" publicadas"
|
|
3948
|
+
] }),
|
|
3949
|
+
r.status === "success" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
3950
|
+
] }, r.id)) })
|
|
3951
|
+
] })
|
|
3952
|
+
] });
|
|
3953
|
+
}
|
|
3954
|
+
var ranklayerConnectorDef = {
|
|
3955
|
+
id: "ranklayer",
|
|
3956
|
+
hostPluginId: "marketing",
|
|
3957
|
+
name: "RankLayer",
|
|
3958
|
+
description: "Publica\xE7\xE3o de conte\xFAdo e p\xE1ginas de SEO por servi\xE7o/bairro para ser encontrado no Google e em assistentes de IA.",
|
|
3959
|
+
icon: "Search",
|
|
3960
|
+
authKind: "api-key",
|
|
3961
|
+
fields: [
|
|
3962
|
+
{ key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
|
|
3963
|
+
{ key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
|
|
3964
|
+
],
|
|
3965
|
+
async getStatus() {
|
|
3966
|
+
try {
|
|
3967
|
+
const found = await provider.getIntegration();
|
|
3968
|
+
return { connected: !!found?.apiKey, detail: found?.siteDomain };
|
|
3969
|
+
} catch {
|
|
3970
|
+
return { connected: false };
|
|
3971
|
+
}
|
|
3972
|
+
},
|
|
3973
|
+
testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
|
|
3974
|
+
saveConnection: async (values) => {
|
|
3975
|
+
await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
|
|
3976
|
+
},
|
|
3977
|
+
disconnect: () => provider.disconnect(),
|
|
3978
|
+
ExtraPanel: RankLayerExtraPanel
|
|
3979
|
+
};
|
|
3528
3980
|
|
|
3529
3981
|
// src/locales/en.ts
|
|
3530
3982
|
var en = {
|
|
@@ -3884,6 +4336,8 @@ var DEFAULT_LABELS = {
|
|
|
3884
4336
|
funnel: "Funnel",
|
|
3885
4337
|
landingPages: "Landing pages",
|
|
3886
4338
|
content: "Content",
|
|
4339
|
+
blog: "Blog",
|
|
4340
|
+
blogCategories: "Categorias",
|
|
3887
4341
|
settings: "Settings"
|
|
3888
4342
|
};
|
|
3889
4343
|
function resolveConfig(options) {
|
|
@@ -3905,9 +4359,9 @@ function createSafeProvider(config, domain) {
|
|
|
3905
4359
|
function createMarketingPlugin(options) {
|
|
3906
4360
|
core.registerTranslations(marketingLocales);
|
|
3907
4361
|
const { config, domain } = resolveConfig(options);
|
|
3908
|
-
const
|
|
3909
|
-
const store = createMarketingStore(
|
|
3910
|
-
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
|
|
4362
|
+
const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
|
|
4363
|
+
const store = createMarketingStore(provider2);
|
|
4364
|
+
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
|
|
3911
4365
|
const contentProvider = options?.contentPlanner?.dataProvider ?? core.createSafeDataProvider(
|
|
3912
4366
|
() => createSupabaseContentPlannerProvider(),
|
|
3913
4367
|
() => createMockContentPlannerProvider()
|
|
@@ -3915,8 +4369,8 @@ function createMarketingPlugin(options) {
|
|
|
3915
4369
|
const contentStore = createContentPlannerStore(contentProvider);
|
|
3916
4370
|
const PageComponent = () => React8__default.default.createElement(
|
|
3917
4371
|
MarketingContextProvider,
|
|
3918
|
-
{ config, provider, store },
|
|
3919
|
-
React8__default.default.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
|
|
4372
|
+
{ config, provider: provider2, store },
|
|
4373
|
+
React8__default.default.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
|
|
3920
4374
|
);
|
|
3921
4375
|
PageComponent.displayName = "MarketingPage";
|
|
3922
4376
|
const marketingRegistries = [
|
|
@@ -3925,12 +4379,14 @@ function createMarketingPlugin(options) {
|
|
|
3925
4379
|
];
|
|
3926
4380
|
const SettingsComponent = () => React8__default.default.createElement(
|
|
3927
4381
|
MarketingContextProvider,
|
|
3928
|
-
{ config, provider, store },
|
|
4382
|
+
{ config, provider: provider2, store },
|
|
3929
4383
|
React8__default.default.createElement(saas.PluginSettingsPanel, {
|
|
3930
4384
|
title: config.labels.settings ?? config.labels.pageTitle,
|
|
3931
4385
|
subtitle: config.labels.pageSubtitle,
|
|
3932
4386
|
generalSettings: React8__default.default.createElement(SettingsView),
|
|
3933
4387
|
registries: marketingRegistries,
|
|
4388
|
+
// Enables the "Integrações" tab (renders manifest connectors for this host).
|
|
4389
|
+
hostPluginId: "marketing",
|
|
3934
4390
|
routeBase: "/settings/marketing"
|
|
3935
4391
|
})
|
|
3936
4392
|
);
|
|
@@ -3950,7 +4406,8 @@ function createMarketingPlugin(options) {
|
|
|
3950
4406
|
declaredFeatures: [
|
|
3951
4407
|
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
3952
4408
|
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
3953
|
-
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
|
|
4409
|
+
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
|
|
4410
|
+
...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
|
|
3954
4411
|
],
|
|
3955
4412
|
navigation: [
|
|
3956
4413
|
{
|
|
@@ -4004,8 +4461,17 @@ function createMarketingPlugin(options) {
|
|
|
4004
4461
|
version: "1.2.0",
|
|
4005
4462
|
sql: MIGRATION_003_RECORDING_OPS,
|
|
4006
4463
|
description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
|
|
4464
|
+
},
|
|
4465
|
+
{
|
|
4466
|
+
id: "marketing-004-ranklayer",
|
|
4467
|
+
version: "1.3.0",
|
|
4468
|
+
sql: MIGRATION_004_RANKLAYER,
|
|
4469
|
+
description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
|
|
4007
4470
|
}
|
|
4008
4471
|
],
|
|
4472
|
+
// RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
|
|
4473
|
+
// (control plane only); the real sync lands via an external PR (RANKLAYER.md).
|
|
4474
|
+
connectors: [ranklayerConnectorDef],
|
|
4009
4475
|
aiTools: [
|
|
4010
4476
|
{
|
|
4011
4477
|
id: "marketing.channel-performance",
|