@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.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React8, { useEffect } from 'react';
|
|
2
|
-
import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation } from '@fayz-ai/core';
|
|
3
|
-
import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore } from '@fayz-ai/saas';
|
|
4
|
-
import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor,
|
|
5
|
-
import {
|
|
6
|
-
import { ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X,
|
|
1
|
+
import React8, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, resolveDataProvider } from '@fayz-ai/core';
|
|
3
|
+
import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore, CrudFormPage, CrudListView } from '@fayz-ai/saas';
|
|
4
|
+
import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, SegmentedControl, DashboardCanvas, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, toast, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import { Sparkles, History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
|
|
7
7
|
import { createStore } from 'zustand/vanilla';
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
@@ -213,11 +213,11 @@ function ChannelDetailView({ channelId, onBack }) {
|
|
|
213
213
|
const range = useMarketingStore((s) => s.range);
|
|
214
214
|
const channels = useMarketingStore((s) => s.channels);
|
|
215
215
|
const lookup = useChannelLookup();
|
|
216
|
-
const
|
|
216
|
+
const provider2 = useMarketingProvider();
|
|
217
217
|
const [campaigns, setCampaigns] = React8.useState([]);
|
|
218
218
|
React8.useEffect(() => {
|
|
219
|
-
void
|
|
220
|
-
}, [
|
|
219
|
+
void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
|
|
220
|
+
}, [provider2, range, channelId]);
|
|
221
221
|
const perf = channels.find((c) => c.channelId === channelId);
|
|
222
222
|
const channel = lookup.get(channelId);
|
|
223
223
|
const columns = React8.useMemo(() => [
|
|
@@ -1962,7 +1962,7 @@ function toDraft2(post) {
|
|
|
1962
1962
|
}
|
|
1963
1963
|
function PostPage({ postId, onBack }) {
|
|
1964
1964
|
const t = useTranslation();
|
|
1965
|
-
const
|
|
1965
|
+
const provider2 = useContentPlannerProvider();
|
|
1966
1966
|
const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
|
|
1967
1967
|
const savePost = useContentPlannerStore((s) => s.savePost);
|
|
1968
1968
|
const deletePost = useContentPlannerStore((s) => s.deletePost);
|
|
@@ -1979,13 +1979,13 @@ function PostPage({ postId, onBack }) {
|
|
|
1979
1979
|
setDraft(toDraft2(storePost));
|
|
1980
1980
|
return;
|
|
1981
1981
|
}
|
|
1982
|
-
void
|
|
1982
|
+
void provider2.getPost(postId).then((fetched) => {
|
|
1983
1983
|
if (fetched) {
|
|
1984
1984
|
setPost(fetched);
|
|
1985
1985
|
setDraft(toDraft2(fetched));
|
|
1986
1986
|
}
|
|
1987
1987
|
});
|
|
1988
|
-
}, [postId, storePost, post?.id,
|
|
1988
|
+
}, [postId, storePost, post?.id, provider2]);
|
|
1989
1989
|
if (!post || !draft) {
|
|
1990
1990
|
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
|
|
1991
1991
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
@@ -2172,6 +2172,239 @@ function PostPage({ postId, onBack }) {
|
|
|
2172
2172
|
post.id
|
|
2173
2173
|
);
|
|
2174
2174
|
}
|
|
2175
|
+
|
|
2176
|
+
// src/data/blogEntities.tsx
|
|
2177
|
+
var BLOG_POSTS_TABLE = "plg_blog_posts";
|
|
2178
|
+
var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
|
|
2179
|
+
function buildBlogPostEntity() {
|
|
2180
|
+
const fields = [
|
|
2181
|
+
{ key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2182
|
+
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
|
|
2183
|
+
{ key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
|
|
2184
|
+
{ key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
|
|
2185
|
+
{ key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
|
|
2186
|
+
{ key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
|
|
2187
|
+
{ key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2188
|
+
{ key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
|
|
2189
|
+
{ key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
|
|
2190
|
+
{ key: "authorName", label: "Author name", type: "text", showInTable: false },
|
|
2191
|
+
{ key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
|
|
2192
|
+
{ key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
|
|
2193
|
+
{ key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
|
|
2194
|
+
];
|
|
2195
|
+
return {
|
|
2196
|
+
name: "Post",
|
|
2197
|
+
namePlural: "Posts",
|
|
2198
|
+
icon: "Newspaper",
|
|
2199
|
+
fields,
|
|
2200
|
+
displayField: "title",
|
|
2201
|
+
subtitleField: "slug",
|
|
2202
|
+
facets: [{ field: "status", allLabel: "All" }],
|
|
2203
|
+
data: {
|
|
2204
|
+
table: BLOG_POSTS_TABLE,
|
|
2205
|
+
tenantScoped: true,
|
|
2206
|
+
searchColumns: ["title", "slug", "excerpt"]
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
function buildBlogCategoryEntity() {
|
|
2211
|
+
const fields = [
|
|
2212
|
+
{ key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
|
|
2213
|
+
{ key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
|
|
2214
|
+
{ key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
|
|
2215
|
+
];
|
|
2216
|
+
return {
|
|
2217
|
+
name: "Category",
|
|
2218
|
+
namePlural: "Categories",
|
|
2219
|
+
icon: "Tags",
|
|
2220
|
+
fields,
|
|
2221
|
+
displayField: "name",
|
|
2222
|
+
data: {
|
|
2223
|
+
table: BLOG_CATEGORIES_TABLE,
|
|
2224
|
+
tenantScoped: true,
|
|
2225
|
+
searchColumns: ["name", "slug"]
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
function useEntityCrud(entity) {
|
|
2230
|
+
const provider2 = useMemo(() => resolveDataProvider(entity), [entity]);
|
|
2231
|
+
const [items, setItems] = useState(null);
|
|
2232
|
+
const [total, setTotal] = useState(0);
|
|
2233
|
+
const fetch = useCallback(async (query = {}) => {
|
|
2234
|
+
try {
|
|
2235
|
+
const res = await provider2.list(query);
|
|
2236
|
+
setItems(res.data);
|
|
2237
|
+
setTotal(res.total);
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
|
|
2240
|
+
setItems([]);
|
|
2241
|
+
setTotal(0);
|
|
2242
|
+
}
|
|
2243
|
+
}, [provider2]);
|
|
2244
|
+
const getById = useCallback(async (id) => {
|
|
2245
|
+
const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
|
|
2246
|
+
return res.data[0] ?? null;
|
|
2247
|
+
}, [provider2]);
|
|
2248
|
+
const create = useCallback((data) => provider2.create(data), [provider2]);
|
|
2249
|
+
const update = useCallback((id, data) => provider2.update(id, data), [provider2]);
|
|
2250
|
+
const remove = useCallback((id) => provider2.remove(id), [provider2]);
|
|
2251
|
+
return { items, total, fetch, getById, create, update, remove };
|
|
2252
|
+
}
|
|
2253
|
+
function PostsList({ navigate }) {
|
|
2254
|
+
const entity = useMemo(() => buildBlogPostEntity(), []);
|
|
2255
|
+
const { items, total, fetch } = useEntityCrud(entity);
|
|
2256
|
+
const [search, setSearch] = useState("");
|
|
2257
|
+
const [statusFilter, setStatusFilter] = useState();
|
|
2258
|
+
const [loadedOnce, setLoadedOnce] = useState(false);
|
|
2259
|
+
useEffect(() => {
|
|
2260
|
+
let active = true;
|
|
2261
|
+
Promise.resolve(
|
|
2262
|
+
fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
|
|
2263
|
+
).finally(() => {
|
|
2264
|
+
if (active) setLoadedOnce(true);
|
|
2265
|
+
});
|
|
2266
|
+
return () => {
|
|
2267
|
+
active = false;
|
|
2268
|
+
};
|
|
2269
|
+
}, [search, statusFilter, fetch]);
|
|
2270
|
+
const facets = useMemo(() => [{
|
|
2271
|
+
field: "status",
|
|
2272
|
+
allLabel: "All",
|
|
2273
|
+
options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
|
|
2274
|
+
}], []);
|
|
2275
|
+
return /* @__PURE__ */ jsx(
|
|
2276
|
+
CrudListView,
|
|
2277
|
+
{
|
|
2278
|
+
entityDef: entity,
|
|
2279
|
+
items: loadedOnce ? items : null,
|
|
2280
|
+
total,
|
|
2281
|
+
search,
|
|
2282
|
+
onSearchChange: setSearch,
|
|
2283
|
+
searchPlaceholder: "Search posts\u2026",
|
|
2284
|
+
facets,
|
|
2285
|
+
activeFilters: { status: statusFilter },
|
|
2286
|
+
onFacetChange: (_field, value) => setStatusFilter(value),
|
|
2287
|
+
onNew: () => navigate("blog-new"),
|
|
2288
|
+
addLabel: "New post",
|
|
2289
|
+
onRowClick: (row) => navigate(`blog-post:${row.id}`)
|
|
2290
|
+
}
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
function CategoriesList({ navigate }) {
|
|
2294
|
+
const entity = useMemo(() => buildBlogCategoryEntity(), []);
|
|
2295
|
+
const { items, total, fetch } = useEntityCrud(entity);
|
|
2296
|
+
const [search, setSearch] = useState("");
|
|
2297
|
+
const [loadedOnce, setLoadedOnce] = useState(false);
|
|
2298
|
+
useEffect(() => {
|
|
2299
|
+
let active = true;
|
|
2300
|
+
Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
|
|
2301
|
+
if (active) setLoadedOnce(true);
|
|
2302
|
+
});
|
|
2303
|
+
return () => {
|
|
2304
|
+
active = false;
|
|
2305
|
+
};
|
|
2306
|
+
}, [search, fetch]);
|
|
2307
|
+
return /* @__PURE__ */ jsx(
|
|
2308
|
+
CrudListView,
|
|
2309
|
+
{
|
|
2310
|
+
entityDef: entity,
|
|
2311
|
+
items: loadedOnce ? items : null,
|
|
2312
|
+
total,
|
|
2313
|
+
search,
|
|
2314
|
+
onSearchChange: setSearch,
|
|
2315
|
+
searchPlaceholder: "Search categories\u2026",
|
|
2316
|
+
onNew: () => navigate("blog-cat-new"),
|
|
2317
|
+
addLabel: "New category",
|
|
2318
|
+
onRowClick: (row) => navigate(`blog-cat:${row.id}`)
|
|
2319
|
+
}
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
function BlogAdminView({ navigate }) {
|
|
2323
|
+
const { labels } = useMarketingConfig();
|
|
2324
|
+
const [lens, setLens] = useState("posts");
|
|
2325
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2326
|
+
/* @__PURE__ */ jsx(
|
|
2327
|
+
SegmentedControl,
|
|
2328
|
+
{
|
|
2329
|
+
"aria-label": "Blog section",
|
|
2330
|
+
options: [
|
|
2331
|
+
{ value: "posts", label: labels.blog },
|
|
2332
|
+
{ value: "categories", label: labels.blogCategories }
|
|
2333
|
+
],
|
|
2334
|
+
value: lens,
|
|
2335
|
+
onChange: (v) => setLens(v)
|
|
2336
|
+
}
|
|
2337
|
+
),
|
|
2338
|
+
lens === "posts" ? /* @__PURE__ */ jsx(PostsList, { navigate }) : /* @__PURE__ */ jsx(CategoriesList, { navigate })
|
|
2339
|
+
] });
|
|
2340
|
+
}
|
|
2341
|
+
function BlogEntityForm({
|
|
2342
|
+
entity,
|
|
2343
|
+
editId,
|
|
2344
|
+
parentLabel,
|
|
2345
|
+
onDone
|
|
2346
|
+
}) {
|
|
2347
|
+
const { getById, create, update, remove } = useEntityCrud(entity);
|
|
2348
|
+
const isEdit = !!editId;
|
|
2349
|
+
const namePlural = entity.namePlural ?? entity.name + "s";
|
|
2350
|
+
const [initialData, setInitialData] = React8.useState(isEdit ? null : {});
|
|
2351
|
+
React8.useEffect(() => {
|
|
2352
|
+
if (!editId) return;
|
|
2353
|
+
let cancelled = false;
|
|
2354
|
+
(async () => {
|
|
2355
|
+
const item = await getById(editId);
|
|
2356
|
+
if (!cancelled) setInitialData(item ?? {});
|
|
2357
|
+
})();
|
|
2358
|
+
return () => {
|
|
2359
|
+
cancelled = true;
|
|
2360
|
+
};
|
|
2361
|
+
}, [editId, getById]);
|
|
2362
|
+
async function save(values) {
|
|
2363
|
+
try {
|
|
2364
|
+
if (isEdit) await update(editId, values);
|
|
2365
|
+
else await create(values);
|
|
2366
|
+
toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
|
|
2367
|
+
onDone();
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
toast.error(`Failed to save ${entity.name.toLowerCase()}`);
|
|
2370
|
+
throw err;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
async function del() {
|
|
2374
|
+
if (!editId) return;
|
|
2375
|
+
try {
|
|
2376
|
+
await remove(editId);
|
|
2377
|
+
toast.success(`${entity.name} deleted`);
|
|
2378
|
+
onDone();
|
|
2379
|
+
} catch {
|
|
2380
|
+
toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
|
|
2384
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2385
|
+
/* @__PURE__ */ jsx(
|
|
2386
|
+
SubpageHeader,
|
|
2387
|
+
{
|
|
2388
|
+
title: String(title),
|
|
2389
|
+
onBack: onDone,
|
|
2390
|
+
parentLabel,
|
|
2391
|
+
actions: isEdit ? /* @__PURE__ */ jsx(Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
|
|
2392
|
+
}
|
|
2393
|
+
),
|
|
2394
|
+
initialData !== null && /* @__PURE__ */ jsx(
|
|
2395
|
+
CrudFormPage,
|
|
2396
|
+
{
|
|
2397
|
+
entityDef: entity,
|
|
2398
|
+
mode: isEdit ? "edit" : "create",
|
|
2399
|
+
initialData,
|
|
2400
|
+
namePlural,
|
|
2401
|
+
onCancel: onDone,
|
|
2402
|
+
onSubmit: save,
|
|
2403
|
+
hideBreadcrumb: true
|
|
2404
|
+
}
|
|
2405
|
+
)
|
|
2406
|
+
] });
|
|
2407
|
+
}
|
|
2175
2408
|
function buildNav(config, view, navigate) {
|
|
2176
2409
|
const items = [
|
|
2177
2410
|
{ id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
|
|
@@ -2203,9 +2436,18 @@ function buildNav(config, view, navigate) {
|
|
|
2203
2436
|
onClick: () => navigate("content")
|
|
2204
2437
|
});
|
|
2205
2438
|
}
|
|
2439
|
+
if (config.modules.blog) {
|
|
2440
|
+
items.push({
|
|
2441
|
+
id: "blog",
|
|
2442
|
+
label: config.labels.blog,
|
|
2443
|
+
icon: "Newspaper",
|
|
2444
|
+
active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
|
|
2445
|
+
onClick: () => navigate("blog")
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2206
2448
|
return items;
|
|
2207
2449
|
}
|
|
2208
|
-
function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
|
|
2450
|
+
function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
|
|
2209
2451
|
const t = useTranslation();
|
|
2210
2452
|
const { view, direction, navigate } = useModuleNavigation("/marketing", {
|
|
2211
2453
|
overview: 0,
|
|
@@ -2214,15 +2456,22 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
|
|
|
2214
2456
|
funnel: 0,
|
|
2215
2457
|
"landing-pages": 0,
|
|
2216
2458
|
content: 0,
|
|
2459
|
+
blog: 0,
|
|
2217
2460
|
"channel-detail": 1,
|
|
2218
2461
|
"content-post": 1,
|
|
2219
|
-
"content-brief": 1
|
|
2462
|
+
"content-brief": 1,
|
|
2463
|
+
"blog-new": 1,
|
|
2464
|
+
"blog-post": 1,
|
|
2465
|
+
"blog-cat-new": 1,
|
|
2466
|
+
"blog-cat": 1
|
|
2220
2467
|
}, "overview");
|
|
2221
2468
|
React8.useEffect(() => {
|
|
2222
2469
|
void store.getState().load();
|
|
2223
2470
|
}, [store]);
|
|
2224
2471
|
const isOverview = view === "overview";
|
|
2225
2472
|
const nav = buildNav(config, view, navigate);
|
|
2473
|
+
const blogPostEntity = React8.useMemo(() => buildBlogPostEntity(), []);
|
|
2474
|
+
const blogCategoryEntity = React8.useMemo(() => buildBlogCategoryEntity(), []);
|
|
2226
2475
|
const quickActions = React8.useMemo(() => [
|
|
2227
2476
|
{ id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
|
|
2228
2477
|
], [t, config.labels.campaigns]);
|
|
@@ -2235,9 +2484,14 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
|
|
|
2235
2484
|
{ id: "content", render: () => /* @__PURE__ */ jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
|
|
2236
2485
|
{ id: "content-post", render: ({ id }) => /* @__PURE__ */ jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
|
|
2237
2486
|
{ id: "content-brief", render: () => /* @__PURE__ */ jsx(PlanBriefView, { onBack: () => navigate("content") }) },
|
|
2487
|
+
{ id: "blog", render: () => /* @__PURE__ */ jsx(BlogAdminView, { navigate }) },
|
|
2488
|
+
{ id: "blog-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2489
|
+
{ id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
|
|
2490
|
+
{ id: "blog-cat-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2491
|
+
{ id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
|
|
2238
2492
|
{ id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
|
|
2239
2493
|
], "overview");
|
|
2240
|
-
return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
|
|
2494
|
+
return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
|
|
2241
2495
|
ModulePage,
|
|
2242
2496
|
{
|
|
2243
2497
|
title: config.labels.pageTitle,
|
|
@@ -2512,7 +2766,7 @@ var AGENCY = {
|
|
|
2512
2766
|
{ id: "referral", label: "Referral", icon: "Users", kind: "referral" },
|
|
2513
2767
|
{ id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
|
|
2514
2768
|
],
|
|
2515
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
|
|
2769
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2516
2770
|
};
|
|
2517
2771
|
var BEAUTY = {
|
|
2518
2772
|
conversion: {
|
|
@@ -2530,7 +2784,7 @@ var BEAUTY = {
|
|
|
2530
2784
|
{ id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
|
|
2531
2785
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2532
2786
|
],
|
|
2533
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
|
|
2787
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
|
|
2534
2788
|
};
|
|
2535
2789
|
var RESTO = {
|
|
2536
2790
|
conversion: {
|
|
@@ -2548,7 +2802,7 @@ var RESTO = {
|
|
|
2548
2802
|
{ id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
|
|
2549
2803
|
{ id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
|
|
2550
2804
|
],
|
|
2551
|
-
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
|
|
2805
|
+
modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
|
|
2552
2806
|
};
|
|
2553
2807
|
var MARKETING_PRESETS = {
|
|
2554
2808
|
agency: AGENCY,
|
|
@@ -3209,7 +3463,7 @@ function createSupabaseContentPlannerProvider() {
|
|
|
3209
3463
|
}
|
|
3210
3464
|
};
|
|
3211
3465
|
}
|
|
3212
|
-
function createMarketingStore(
|
|
3466
|
+
function createMarketingStore(provider2) {
|
|
3213
3467
|
return createStore((set, get) => ({
|
|
3214
3468
|
range: "30d",
|
|
3215
3469
|
overview: null,
|
|
@@ -3222,11 +3476,11 @@ function createMarketingStore(provider) {
|
|
|
3222
3476
|
const range = get().range;
|
|
3223
3477
|
set({ loading: true });
|
|
3224
3478
|
const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3479
|
+
provider2.overview({ range }),
|
|
3480
|
+
provider2.channelPerformance({ range }),
|
|
3481
|
+
provider2.listCampaigns({ range }),
|
|
3482
|
+
provider2.funnel({ range }),
|
|
3483
|
+
provider2.landingPages({ range })
|
|
3230
3484
|
]);
|
|
3231
3485
|
set({ overview, channels, campaigns, funnel, landingPages, loading: false });
|
|
3232
3486
|
},
|
|
@@ -3235,21 +3489,21 @@ function createMarketingStore(provider) {
|
|
|
3235
3489
|
await get().load();
|
|
3236
3490
|
},
|
|
3237
3491
|
async saveCampaign(input) {
|
|
3238
|
-
const created = await
|
|
3492
|
+
const created = await provider2.saveCampaign(input);
|
|
3239
3493
|
await get().load();
|
|
3240
3494
|
return created;
|
|
3241
3495
|
},
|
|
3242
3496
|
async deleteCampaign(id) {
|
|
3243
|
-
await
|
|
3497
|
+
await provider2.deleteCampaign(id);
|
|
3244
3498
|
await get().load();
|
|
3245
3499
|
}
|
|
3246
3500
|
}));
|
|
3247
3501
|
}
|
|
3248
|
-
function createContentPlannerStore(
|
|
3502
|
+
function createContentPlannerStore(provider2) {
|
|
3249
3503
|
async function loadPlansFor(accountId) {
|
|
3250
|
-
const plans = await
|
|
3504
|
+
const plans = await provider2.listPlans(accountId);
|
|
3251
3505
|
const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
|
|
3252
|
-
const posts = activePlan ? await
|
|
3506
|
+
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3253
3507
|
return { plans, activePlan, posts };
|
|
3254
3508
|
}
|
|
3255
3509
|
return createStore((set, get) => ({
|
|
@@ -3269,7 +3523,7 @@ function createContentPlannerStore(provider) {
|
|
|
3269
3523
|
if (get().loading) return;
|
|
3270
3524
|
set({ loading: true });
|
|
3271
3525
|
try {
|
|
3272
|
-
const accounts = await
|
|
3526
|
+
const accounts = await provider2.listAccounts();
|
|
3273
3527
|
const current = get().activeAccountId;
|
|
3274
3528
|
const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
|
|
3275
3529
|
const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
|
|
@@ -3300,19 +3554,19 @@ function createContentPlannerStore(provider) {
|
|
|
3300
3554
|
},
|
|
3301
3555
|
async selectPlan(planId) {
|
|
3302
3556
|
const plan = get().plans.find((p) => p.id === planId) ?? null;
|
|
3303
|
-
const posts = plan ? await
|
|
3557
|
+
const posts = plan ? await provider2.listPosts(plan.id) : [];
|
|
3304
3558
|
set({ activePlan: plan, posts });
|
|
3305
3559
|
},
|
|
3306
3560
|
async saveAccount(input) {
|
|
3307
|
-
const account = await
|
|
3308
|
-
const accounts = await
|
|
3561
|
+
const account = await provider2.saveAccount(input);
|
|
3562
|
+
const accounts = await provider2.listAccounts();
|
|
3309
3563
|
set({ accounts });
|
|
3310
3564
|
if (!get().activeAccountId) await get().setAccount(account.id);
|
|
3311
3565
|
return account;
|
|
3312
3566
|
},
|
|
3313
3567
|
async deleteAccount(id) {
|
|
3314
|
-
await
|
|
3315
|
-
const accounts = await
|
|
3568
|
+
await provider2.deleteAccount(id);
|
|
3569
|
+
const accounts = await provider2.listAccounts();
|
|
3316
3570
|
const wasActive = get().activeAccountId === id;
|
|
3317
3571
|
set({ accounts });
|
|
3318
3572
|
if (wasActive) {
|
|
@@ -3325,30 +3579,30 @@ function createContentPlannerStore(provider) {
|
|
|
3325
3579
|
}
|
|
3326
3580
|
},
|
|
3327
3581
|
async savePlan(input) {
|
|
3328
|
-
const plan = await
|
|
3582
|
+
const plan = await provider2.savePlan(input);
|
|
3329
3583
|
const accountId = get().activeAccountId;
|
|
3330
3584
|
if (accountId) {
|
|
3331
|
-
const plans = await
|
|
3585
|
+
const plans = await provider2.listPlans(accountId);
|
|
3332
3586
|
const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
|
|
3333
|
-
const posts = activePlan ? await
|
|
3587
|
+
const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
|
|
3334
3588
|
set({ plans, activePlan, posts });
|
|
3335
3589
|
}
|
|
3336
3590
|
return plan;
|
|
3337
3591
|
},
|
|
3338
3592
|
async savePost(input) {
|
|
3339
|
-
const post = await
|
|
3593
|
+
const post = await provider2.savePost(input);
|
|
3340
3594
|
const planId = get().activePlan?.id;
|
|
3341
3595
|
if (planId && post.planId === planId) {
|
|
3342
|
-
const posts = await
|
|
3596
|
+
const posts = await provider2.listPosts(planId);
|
|
3343
3597
|
set({ posts });
|
|
3344
3598
|
}
|
|
3345
3599
|
return post;
|
|
3346
3600
|
},
|
|
3347
3601
|
async deletePost(id) {
|
|
3348
|
-
await
|
|
3602
|
+
await provider2.deletePost(id);
|
|
3349
3603
|
const planId = get().activePlan?.id;
|
|
3350
3604
|
if (planId) {
|
|
3351
|
-
const posts = await
|
|
3605
|
+
const posts = await provider2.listPosts(planId);
|
|
3352
3606
|
set({ posts });
|
|
3353
3607
|
}
|
|
3354
3608
|
}
|
|
@@ -3519,6 +3773,204 @@ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authen
|
|
|
3519
3773
|
CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3520
3774
|
CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
|
|
3521
3775
|
`;
|
|
3776
|
+
var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
|
|
3777
|
+
-- Control-plane state for the RankLayer connector rendered in Marketing \u2192
|
|
3778
|
+
-- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
|
|
3779
|
+
-- (edge function + RankLayer API calls) lands via an external PR \u2014 see
|
|
3780
|
+
-- src/integrations/ranklayer/RANKLAYER.md.
|
|
3781
|
+
|
|
3782
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
|
|
3783
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3784
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3785
|
+
api_key text,
|
|
3786
|
+
site_domain text,
|
|
3787
|
+
active boolean NOT NULL DEFAULT true,
|
|
3788
|
+
last_sync_at timestamptz,
|
|
3789
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3790
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3791
|
+
);
|
|
3792
|
+
ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
|
|
3793
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
|
|
3794
|
+
ON public.plg_marketing_ranklayer_integrations(tenant_id);
|
|
3795
|
+
|
|
3796
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
|
|
3797
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3798
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
3799
|
+
integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
|
|
3800
|
+
status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
|
|
3801
|
+
message text,
|
|
3802
|
+
fetched integer NOT NULL DEFAULT 0,
|
|
3803
|
+
written integer NOT NULL DEFAULT 0,
|
|
3804
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3805
|
+
);
|
|
3806
|
+
ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
|
|
3807
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
|
|
3808
|
+
ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
|
|
3809
|
+
|
|
3810
|
+
-- RLS: authenticated tenant CRUD
|
|
3811
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
|
|
3812
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
|
|
3813
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
|
|
3814
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
|
|
3815
|
+
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()));
|
|
3816
|
+
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()));
|
|
3817
|
+
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()));
|
|
3818
|
+
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()));
|
|
3819
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
|
|
3820
|
+
|
|
3821
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
|
|
3822
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
|
|
3823
|
+
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()));
|
|
3824
|
+
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()));
|
|
3825
|
+
GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
|
|
3826
|
+
`;
|
|
3827
|
+
var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
|
|
3828
|
+
var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
|
|
3829
|
+
function getClient2() {
|
|
3830
|
+
const supabase = getSupabaseClientOptional();
|
|
3831
|
+
if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
|
|
3832
|
+
return supabase;
|
|
3833
|
+
}
|
|
3834
|
+
function requireTenantId2() {
|
|
3835
|
+
const tenantId = getActiveTenantId();
|
|
3836
|
+
if (!tenantId) throw new Error("[ranklayer] No active tenant");
|
|
3837
|
+
return tenantId;
|
|
3838
|
+
}
|
|
3839
|
+
function toIntegration(row) {
|
|
3840
|
+
return {
|
|
3841
|
+
id: row.id,
|
|
3842
|
+
apiKey: row.api_key ?? void 0,
|
|
3843
|
+
siteDomain: row.site_domain ?? void 0,
|
|
3844
|
+
active: row.active ?? true,
|
|
3845
|
+
lastSyncAt: row.last_sync_at ?? void 0
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
function createRankLayerProvider() {
|
|
3849
|
+
return {
|
|
3850
|
+
async getIntegration() {
|
|
3851
|
+
const tenantId = getActiveTenantId();
|
|
3852
|
+
if (!tenantId) return null;
|
|
3853
|
+
const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
|
|
3854
|
+
return data ? toIntegration(data) : null;
|
|
3855
|
+
},
|
|
3856
|
+
async saveIntegration(values) {
|
|
3857
|
+
const tenantId = requireTenantId2();
|
|
3858
|
+
const payload = {
|
|
3859
|
+
tenant_id: tenantId,
|
|
3860
|
+
api_key: values.apiKey,
|
|
3861
|
+
site_domain: values.siteDomain ?? null,
|
|
3862
|
+
active: true,
|
|
3863
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3864
|
+
};
|
|
3865
|
+
const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
|
|
3866
|
+
if (error) throw error;
|
|
3867
|
+
return toIntegration(data);
|
|
3868
|
+
},
|
|
3869
|
+
async disconnect() {
|
|
3870
|
+
const tenantId = getActiveTenantId();
|
|
3871
|
+
if (!tenantId) return;
|
|
3872
|
+
await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
|
|
3873
|
+
},
|
|
3874
|
+
// SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
|
|
3875
|
+
// flow can be exercised. Replace with a real credential check in the PR.
|
|
3876
|
+
async testConnection(values) {
|
|
3877
|
+
if (!values.apiKey || values.apiKey.trim().length < 8) {
|
|
3878
|
+
return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
|
|
3879
|
+
}
|
|
3880
|
+
return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
|
|
3881
|
+
},
|
|
3882
|
+
async getSyncLog(integrationId) {
|
|
3883
|
+
const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
|
|
3884
|
+
return (data ?? []).map((r) => ({
|
|
3885
|
+
id: r.id,
|
|
3886
|
+
createdAt: r.created_at,
|
|
3887
|
+
status: r.status,
|
|
3888
|
+
message: r.message ?? void 0,
|
|
3889
|
+
fetched: r.fetched ?? 0,
|
|
3890
|
+
written: r.written ?? 0
|
|
3891
|
+
}));
|
|
3892
|
+
}
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
var provider = createRankLayerProvider();
|
|
3896
|
+
function RankLayerExtraPanel() {
|
|
3897
|
+
const [integration, setIntegration] = useState(null);
|
|
3898
|
+
const [log, setLog] = useState([]);
|
|
3899
|
+
const [loaded, setLoaded] = useState(false);
|
|
3900
|
+
useEffect(() => {
|
|
3901
|
+
let cancelled = false;
|
|
3902
|
+
(async () => {
|
|
3903
|
+
try {
|
|
3904
|
+
const found = await provider.getIntegration();
|
|
3905
|
+
if (cancelled) return;
|
|
3906
|
+
setIntegration(found);
|
|
3907
|
+
if (found) setLog(await provider.getSyncLog(found.id));
|
|
3908
|
+
} catch {
|
|
3909
|
+
} finally {
|
|
3910
|
+
if (!cancelled) setLoaded(true);
|
|
3911
|
+
}
|
|
3912
|
+
})();
|
|
3913
|
+
return () => {
|
|
3914
|
+
cancelled = true;
|
|
3915
|
+
};
|
|
3916
|
+
}, []);
|
|
3917
|
+
if (!loaded || !integration) return null;
|
|
3918
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4 border-t pt-4", children: [
|
|
3919
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
|
|
3920
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
3921
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
3922
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
|
|
3923
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
3924
|
+
"A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
|
|
3925
|
+
integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
|
|
3926
|
+
" Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
|
|
3927
|
+
] })
|
|
3928
|
+
] })
|
|
3929
|
+
] }),
|
|
3930
|
+
log.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
3931
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3932
|
+
/* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
3933
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
|
|
3934
|
+
] }),
|
|
3935
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
|
|
3936
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
|
|
3937
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
|
|
3938
|
+
r.written,
|
|
3939
|
+
"/",
|
|
3940
|
+
r.fetched,
|
|
3941
|
+
" publicadas"
|
|
3942
|
+
] }),
|
|
3943
|
+
r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
3944
|
+
] }, r.id)) })
|
|
3945
|
+
] })
|
|
3946
|
+
] });
|
|
3947
|
+
}
|
|
3948
|
+
var ranklayerConnectorDef = {
|
|
3949
|
+
id: "ranklayer",
|
|
3950
|
+
hostPluginId: "marketing",
|
|
3951
|
+
name: "RankLayer",
|
|
3952
|
+
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.",
|
|
3953
|
+
icon: "Search",
|
|
3954
|
+
authKind: "api-key",
|
|
3955
|
+
fields: [
|
|
3956
|
+
{ key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
|
|
3957
|
+
{ key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
|
|
3958
|
+
],
|
|
3959
|
+
async getStatus() {
|
|
3960
|
+
try {
|
|
3961
|
+
const found = await provider.getIntegration();
|
|
3962
|
+
return { connected: !!found?.apiKey, detail: found?.siteDomain };
|
|
3963
|
+
} catch {
|
|
3964
|
+
return { connected: false };
|
|
3965
|
+
}
|
|
3966
|
+
},
|
|
3967
|
+
testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
|
|
3968
|
+
saveConnection: async (values) => {
|
|
3969
|
+
await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
|
|
3970
|
+
},
|
|
3971
|
+
disconnect: () => provider.disconnect(),
|
|
3972
|
+
ExtraPanel: RankLayerExtraPanel
|
|
3973
|
+
};
|
|
3522
3974
|
|
|
3523
3975
|
// src/locales/en.ts
|
|
3524
3976
|
var en = {
|
|
@@ -3878,6 +4330,8 @@ var DEFAULT_LABELS = {
|
|
|
3878
4330
|
funnel: "Funnel",
|
|
3879
4331
|
landingPages: "Landing pages",
|
|
3880
4332
|
content: "Content",
|
|
4333
|
+
blog: "Blog",
|
|
4334
|
+
blogCategories: "Categorias",
|
|
3881
4335
|
settings: "Settings"
|
|
3882
4336
|
};
|
|
3883
4337
|
function resolveConfig(options) {
|
|
@@ -3899,9 +4353,9 @@ function createSafeProvider(config, domain) {
|
|
|
3899
4353
|
function createMarketingPlugin(options) {
|
|
3900
4354
|
registerTranslations(marketingLocales);
|
|
3901
4355
|
const { config, domain } = resolveConfig(options);
|
|
3902
|
-
const
|
|
3903
|
-
const store = createMarketingStore(
|
|
3904
|
-
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
|
|
4356
|
+
const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
|
|
4357
|
+
const store = createMarketingStore(provider2);
|
|
4358
|
+
const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
|
|
3905
4359
|
const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
|
|
3906
4360
|
() => createSupabaseContentPlannerProvider(),
|
|
3907
4361
|
() => createMockContentPlannerProvider()
|
|
@@ -3909,8 +4363,8 @@ function createMarketingPlugin(options) {
|
|
|
3909
4363
|
const contentStore = createContentPlannerStore(contentProvider);
|
|
3910
4364
|
const PageComponent = () => React8.createElement(
|
|
3911
4365
|
MarketingContextProvider,
|
|
3912
|
-
{ config, provider, store },
|
|
3913
|
-
React8.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
|
|
4366
|
+
{ config, provider: provider2, store },
|
|
4367
|
+
React8.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
|
|
3914
4368
|
);
|
|
3915
4369
|
PageComponent.displayName = "MarketingPage";
|
|
3916
4370
|
const marketingRegistries = [
|
|
@@ -3919,12 +4373,14 @@ function createMarketingPlugin(options) {
|
|
|
3919
4373
|
];
|
|
3920
4374
|
const SettingsComponent = () => React8.createElement(
|
|
3921
4375
|
MarketingContextProvider,
|
|
3922
|
-
{ config, provider, store },
|
|
4376
|
+
{ config, provider: provider2, store },
|
|
3923
4377
|
React8.createElement(PluginSettingsPanel, {
|
|
3924
4378
|
title: config.labels.settings ?? config.labels.pageTitle,
|
|
3925
4379
|
subtitle: config.labels.pageSubtitle,
|
|
3926
4380
|
generalSettings: React8.createElement(SettingsView),
|
|
3927
4381
|
registries: marketingRegistries,
|
|
4382
|
+
// Enables the "Integrações" tab (renders manifest connectors for this host).
|
|
4383
|
+
hostPluginId: "marketing",
|
|
3928
4384
|
routeBase: "/settings/marketing"
|
|
3929
4385
|
})
|
|
3930
4386
|
);
|
|
@@ -3944,7 +4400,8 @@ function createMarketingPlugin(options) {
|
|
|
3944
4400
|
declaredFeatures: [
|
|
3945
4401
|
{ id: "marketing", label: config.labels.pageTitle, group: "Engage" },
|
|
3946
4402
|
...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
|
|
3947
|
-
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
|
|
4403
|
+
...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
|
|
4404
|
+
...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
|
|
3948
4405
|
],
|
|
3949
4406
|
navigation: [
|
|
3950
4407
|
{
|
|
@@ -3998,8 +4455,17 @@ function createMarketingPlugin(options) {
|
|
|
3998
4455
|
version: "1.2.0",
|
|
3999
4456
|
sql: MIGRATION_003_RECORDING_OPS,
|
|
4000
4457
|
description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
|
|
4458
|
+
},
|
|
4459
|
+
{
|
|
4460
|
+
id: "marketing-004-ranklayer",
|
|
4461
|
+
version: "1.3.0",
|
|
4462
|
+
sql: MIGRATION_004_RANKLAYER,
|
|
4463
|
+
description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
|
|
4001
4464
|
}
|
|
4002
4465
|
],
|
|
4466
|
+
// RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
|
|
4467
|
+
// (control plane only); the real sync lands via an external PR (RANKLAYER.md).
|
|
4468
|
+
connectors: [ranklayerConnectorDef],
|
|
4003
4469
|
aiTools: [
|
|
4004
4470
|
{
|
|
4005
4471
|
id: "marketing.channel-performance",
|