@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectorDef.d.ts","sourceRoot":"","sources":["../../../src/integrations/ranklayer/connectorDef.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAmExD,eAAO,MAAM,qBAAqB,EAAE,mBAuBnC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TestConnectionResult } from '@fayz-ai/core';
|
|
2
|
+
import type { RankLayerIntegration, RankLayerSyncLogEntry } from '../types';
|
|
3
|
+
export declare function createRankLayerProvider(): {
|
|
4
|
+
getIntegration(): Promise<RankLayerIntegration | null>;
|
|
5
|
+
saveIntegration(values: {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
siteDomain?: string;
|
|
8
|
+
}): Promise<RankLayerIntegration>;
|
|
9
|
+
disconnect(): Promise<void>;
|
|
10
|
+
testConnection(values: {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
}): Promise<TestConnectionResult>;
|
|
13
|
+
getSyncLog(integrationId: string): Promise<RankLayerSyncLogEntry[]>;
|
|
14
|
+
};
|
|
15
|
+
export type RankLayerProvider = ReturnType<typeof createRankLayerProvider>;
|
|
16
|
+
//# sourceMappingURL=supabase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../../../src/integrations/ranklayer/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAqC3E,wBAAgB,uBAAuB;sBAEX,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;4BAW9B;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC;kBAmBjF,OAAO,CAAC,IAAI,CAAC;2BAQJ;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC;8BAO/C,MAAM,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;EAiB5E;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RankLayerIntegration {
|
|
2
|
+
id: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
siteDomain?: string;
|
|
5
|
+
active: boolean;
|
|
6
|
+
lastSyncAt?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RankLayerSyncLogEntry {
|
|
9
|
+
id: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
status: 'success' | 'partial' | 'error';
|
|
12
|
+
message?: string;
|
|
13
|
+
fetched: number;
|
|
14
|
+
written: number;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/integrations/ranklayer/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;IACvC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB"}
|
|
@@ -2,6 +2,7 @@ export declare const MIGRATION_000_PLG_RENAME = "-- 000_plg_rename.sql \u2014 re
|
|
|
2
2
|
export declare const MIGRATION_001_CONTENT_PLANNER = "-- Marketing Plugin: Content Planner\n-- Prefix: plg_marketing_\n-- Social accounts + content plans + posts (Notion-style markdown pages).\n\nCREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n handle text,\n platform text NOT NULL DEFAULT 'instagram'\n CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,\n name text NOT NULL,\n status text NOT NULL DEFAULT 'active'\n CHECK (status IN ('draft', 'active', 'archived')),\n weeks_count integer NOT NULL DEFAULT 8,\n start_date date,\n objective text,\n tone text,\n pillars jsonb NOT NULL DEFAULT '[]'::jsonb,\n formats jsonb NOT NULL DEFAULT '[]'::jsonb,\n weekly_frequency integer NOT NULL DEFAULT 3,\n brief_md text NOT NULL DEFAULT '',\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,\n week_number integer NOT NULL DEFAULT 1,\n position integer NOT NULL DEFAULT 0,\n title text NOT NULL,\n format text NOT NULL DEFAULT 'reel'\n CHECK (format IN ('reel', 'static', 'carousel', 'story')),\n status text NOT NULL DEFAULT 'idea'\n CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),\n scheduled_date date,\n hook text,\n cta text,\n content_md text NOT NULL DEFAULT '',\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);\n\n-- RLS policies for plg_marketing_social_accounts\nDROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;\nDROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;\nDROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;\nDROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;\nCREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;\n\n-- RLS policies for plg_marketing_content_plans\nDROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;\nDROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;\nDROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;\nDROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;\nCREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;\n\n-- RLS policies for plg_marketing_content_posts\nDROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;\nDROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;\nDROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;\nDROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;\nCREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;\n";
|
|
3
3
|
export declare const MIGRATION_002_MULTI_PLATFORM = "-- Marketing Plugin: multi-platform accounts + per-post targets.\n-- An account is a brand/profile that publishes to MANY platforms (Instagram,\n-- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an\n-- empty array means \"inherit the account's platforms\".\n\nALTER TABLE public.plg_marketing_social_accounts\n ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';\n\n-- Backfill from the legacy single-platform column, then drop it (also drops\n-- its CHECK constraint \u2014 the platform list is UI-driven from here on).\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'\n ) THEN\n UPDATE public.plg_marketing_social_accounts\n SET platforms = ARRAY[platform]\n WHERE platform IS NOT NULL AND platforms = '{instagram}';\n ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;\n END IF;\nEND $$;\n\nALTER TABLE public.plg_marketing_content_posts\n ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';\n";
|
|
4
4
|
export declare const MIGRATION_003_RECORDING_OPS = "-- Marketing Plugin: recording-day ops on posts.\n-- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set\n-- companion for recording day. media_url: uploaded asset (static posts get a\n-- caption + final art instead of a script).\n\nALTER TABLE public.plg_marketing_content_posts\n ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';\n\nALTER TABLE public.plg_marketing_content_posts\n ADD COLUMN IF NOT EXISTS media_url text;\n\n-- Public media bucket for content assets (mirrors the avatars-bucket pattern:\n-- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}\nINSERT INTO storage.buckets (id, name, public)\n VALUES ('mkt-media', 'mkt-media', true)\n ON CONFLICT (id) DO NOTHING;\n\nDROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;\nDROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;\nDROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;\nDROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;\nCREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');\nCREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');\nCREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');\nCREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');\n";
|
|
5
|
+
export declare const MIGRATION_004_RANKLAYER = "-- Marketing Plugin: RankLayer integration (SEO content publishing).\n-- Control-plane state for the RankLayer connector rendered in Marketing \u2192\n-- Integra\u00E7\u00F5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync\n-- (edge function + RankLayer API calls) lands via an external PR \u2014 see\n-- src/integrations/ranklayer/RANKLAYER.md.\n\nCREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n api_key text,\n site_domain text,\n active boolean NOT NULL DEFAULT true,\n last_sync_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant\n ON public.plg_marketing_ranklayer_integrations(tenant_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,\n status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),\n message text,\n fetched integer NOT NULL DEFAULT 0,\n written integer NOT NULL DEFAULT 0,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant\n ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);\n\n-- RLS: authenticated tenant CRUD\nDROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;\nDROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;\nDROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;\nDROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;\nCREATE 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()));\nCREATE 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()));\nCREATE 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()));\nCREATE 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()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;\n\nDROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;\nDROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;\nCREATE 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()));\nCREATE 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()));\nGRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;\n";
|
|
5
6
|
export declare const MIGRATIONS: Array<{
|
|
6
7
|
id: string;
|
|
7
8
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,i6BAepC,CAAA;AAED,eAAO,MAAM,6BAA6B,ivNA8FzC,CAAA;AAED,eAAO,MAAM,4BAA4B,inCAyBxC,CAAA;AAED,eAAO,MAAM,2BAA2B,8/CAyBvC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,i6BAepC,CAAA;AAED,eAAO,MAAM,6BAA6B,ivNA8FzC,CAAA;AAED,eAAO,MAAM,4BAA4B,inCAyBxC,CAAA;AAED,eAAO,MAAM,2BAA2B,8/CAyBvC,CAAA;AAED,eAAO,MAAM,uBAAuB,6tHAkDnC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMzD,CAAA"}
|
package/dist/presets.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface MarketingDomainModules {
|
|
|
7
7
|
landingPages: boolean;
|
|
8
8
|
/** Social content planner — accounts, week board, markdown post pages. */
|
|
9
9
|
contentPlanner: boolean;
|
|
10
|
+
/** Blog backoffice — posts + categories CMS feeding @fayz-ai/plugin-blog. Off by default. */
|
|
11
|
+
blog: boolean;
|
|
10
12
|
/** Outbound — reserved, off by default, implemented in a later milestone. */
|
|
11
13
|
broadcasts: boolean;
|
|
12
14
|
journeys: boolean;
|
package/dist/presets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AASlE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE3D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,YAAY,EAAE,OAAO,CAAA;IACrB,0EAA0E;IAC1E,cAAc,EAAE,OAAO,CAAA;IACvB,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,eAAe,CAAA;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,OAAO,EAAE,sBAAsB,CAAA;CAChC;AA4DD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,eAAe,EAAE,qBAAqB,CAI5E,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAsBjE,CAAA"}
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AASlE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE3D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,YAAY,EAAE,OAAO,CAAA;IACrB,0EAA0E;IAC1E,cAAc,EAAE,OAAO,CAAA;IACvB,6FAA6F;IAC7F,IAAI,EAAE,OAAO,CAAA;IACb,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,eAAe,CAAA;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,OAAO,EAAE,sBAAsB,CAAA;CAChC;AA4DD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,eAAe,EAAE,qBAAqB,CAI5E,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAsBjE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlogAdminView.d.ts","sourceRoot":"","sources":["../../../src/views/blog/BlogAdminView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;AAgF3D,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,qBAkB/E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { EntityDef } from '@fayz-ai/core';
|
|
3
|
+
export declare function BlogEntityForm<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}>({ entity, editId, parentLabel, onDone, }: {
|
|
6
|
+
entity: EntityDef<T>;
|
|
7
|
+
editId?: string;
|
|
8
|
+
parentLabel: string;
|
|
9
|
+
onDone: () => void;
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
//# sourceMappingURL=BlogEntityForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlogEntityForm.d.ts","sourceRoot":"","sources":["../../../src/views/blog/BlogEntityForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAU9C,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EACvD,MAAM,EACN,MAAM,EACN,WAAW,EACX,MAAM,GACP,EAAE;IACD,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB,qBAoEA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EntityDef } from '@fayz-ai/core';
|
|
2
|
+
interface ListQuery {
|
|
3
|
+
search?: string;
|
|
4
|
+
filters?: Record<string, unknown>;
|
|
5
|
+
page?: number;
|
|
6
|
+
pageSize?: number;
|
|
7
|
+
sortBy?: string;
|
|
8
|
+
sortDir?: 'asc' | 'desc';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal store-less CRUD wiring for a plugin that owns its data + routing.
|
|
12
|
+
* Resolves the generic Supabase provider from the EntityDef's `data.table`
|
|
13
|
+
* (mock fallback when Supabase is not configured) and exposes list/get/save/
|
|
14
|
+
* remove. Used by the blog backoffice list + form views — the heavy list/form
|
|
15
|
+
* rendering stays in the shared CrudListView / CrudFormPage.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useEntityCrud<T extends {
|
|
18
|
+
id: string;
|
|
19
|
+
}>(entity: EntityDef<T>): {
|
|
20
|
+
items: T[] | null;
|
|
21
|
+
total: number;
|
|
22
|
+
fetch: (query?: ListQuery) => Promise<void>;
|
|
23
|
+
getById: (id: string) => Promise<T | null>;
|
|
24
|
+
create: (data: Record<string, unknown>) => Promise<T>;
|
|
25
|
+
update: (id: string, data: Record<string, unknown>) => Promise<T>;
|
|
26
|
+
remove: (id: string) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=useEntityCrud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEntityCrud.d.ts","sourceRoot":"","sources":["../../../src/views/blog/useEntityCrud.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;;;oBAKlC,SAAS;kBAcV,MAAM,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAK/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;iBACzB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;iBACrC,MAAM;EAGvC"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "beta"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.8.
|
|
6
|
+
"version": "0.8.1",
|
|
7
7
|
"description": "Fayz SDK — marketing campaigns & broadcasts plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.cjs",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"@tanstack/react-table": "^8.20.0",
|
|
36
36
|
"lucide-react": "^0.400.0",
|
|
37
37
|
"zustand": "^4.5.0",
|
|
38
|
-
"@fayz-ai/
|
|
39
|
-
"@fayz-ai/
|
|
40
|
-
"@fayz-ai/saas": "^0.8.
|
|
38
|
+
"@fayz-ai/core": "^0.8.1",
|
|
39
|
+
"@fayz-ai/ui": "^0.8.1",
|
|
40
|
+
"@fayz-ai/saas": "^0.8.1",
|
|
41
41
|
"@fayz-ai/db": "^0.8.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/MarketingContext.tsx
CHANGED
package/src/MarketingPage.tsx
CHANGED
|
@@ -19,6 +19,9 @@ import type { ContentPlannerUIState } from './views/content/contentStore'
|
|
|
19
19
|
import { ContentView } from './views/content/ContentView'
|
|
20
20
|
import { PostPage } from './views/content/PostPage'
|
|
21
21
|
import { PlanBriefView } from './views/content/PlanBriefView'
|
|
22
|
+
import { BlogAdminView } from './views/blog/BlogAdminView'
|
|
23
|
+
import { BlogEntityForm } from './views/blog/BlogEntityForm'
|
|
24
|
+
import { buildBlogPostEntity, buildBlogCategoryEntity } from './data/blogEntities'
|
|
22
25
|
|
|
23
26
|
function buildNav(
|
|
24
27
|
config: ResolvedMarketingConfig,
|
|
@@ -51,6 +54,14 @@ function buildNav(
|
|
|
51
54
|
onClick: () => navigate('content'),
|
|
52
55
|
})
|
|
53
56
|
}
|
|
57
|
+
if (config.modules.blog) {
|
|
58
|
+
items.push({
|
|
59
|
+
id: 'blog', label: config.labels.blog, icon: 'Newspaper',
|
|
60
|
+
active: view === 'blog' || view === 'blog-new' || view.startsWith('blog-post:')
|
|
61
|
+
|| view === 'blog-cat-new' || view.startsWith('blog-cat:'),
|
|
62
|
+
onClick: () => navigate('blog'),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
54
65
|
return items
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -63,8 +74,9 @@ export function MarketingPage({ config, provider, store, contentProvider, conten
|
|
|
63
74
|
}) {
|
|
64
75
|
const t = useTranslation()
|
|
65
76
|
const { view, direction, navigate } = useModuleNavigation('/marketing', {
|
|
66
|
-
overview: 0, channels: 0, campaigns: 0, funnel: 0, 'landing-pages': 0, content: 0,
|
|
77
|
+
overview: 0, channels: 0, campaigns: 0, funnel: 0, 'landing-pages': 0, content: 0, blog: 0,
|
|
67
78
|
'channel-detail': 1, 'content-post': 1, 'content-brief': 1,
|
|
79
|
+
'blog-new': 1, 'blog-post': 1, 'blog-cat-new': 1, 'blog-cat': 1,
|
|
68
80
|
}, 'overview')
|
|
69
81
|
|
|
70
82
|
React.useEffect(() => { void store.getState().load() }, [store])
|
|
@@ -72,6 +84,10 @@ export function MarketingPage({ config, provider, store, contentProvider, conten
|
|
|
72
84
|
const isOverview = view === 'overview'
|
|
73
85
|
const nav = buildNav(config, view, navigate)
|
|
74
86
|
|
|
87
|
+
// Blog backoffice entities — one shared EntityDef each drives the list + form.
|
|
88
|
+
const blogPostEntity = React.useMemo(() => buildBlogPostEntity(), [])
|
|
89
|
+
const blogCategoryEntity = React.useMemo(() => buildBlogCategoryEntity(), [])
|
|
90
|
+
|
|
75
91
|
const quickActions = React.useMemo<PluginQuickAction[]>(() => [
|
|
76
92
|
{ id: 'new-campaign', label: t('marketing.campaigns.new'), icon: 'Megaphone', description: config.labels.campaigns, action: () => navigate('campaigns') },
|
|
77
93
|
], [t, config.labels.campaigns])
|
|
@@ -85,6 +101,11 @@ export function MarketingPage({ config, provider, store, contentProvider, conten
|
|
|
85
101
|
{ id: 'content', render: () => <ContentView onOpenPost={(id) => navigate(`content-post:${id}`)} /> },
|
|
86
102
|
{ id: 'content-post', render: ({ id }) => <PostPage postId={id!} onBack={() => navigate('content')} /> },
|
|
87
103
|
{ id: 'content-brief', render: () => <PlanBriefView onBack={() => navigate('content')} /> },
|
|
104
|
+
{ id: 'blog', render: () => <BlogAdminView navigate={navigate} /> },
|
|
105
|
+
{ id: 'blog-new', render: () => <BlogEntityForm entity={blogPostEntity} parentLabel={config.labels.blog} onDone={() => navigate('blog')} /> },
|
|
106
|
+
{ id: 'blog-post', render: ({ id }) => <BlogEntityForm entity={blogPostEntity} editId={id!} parentLabel={config.labels.blog} onDone={() => navigate('blog')} /> },
|
|
107
|
+
{ id: 'blog-cat-new', render: () => <BlogEntityForm entity={blogCategoryEntity} parentLabel={config.labels.blogCategories} onDone={() => navigate('blog')} /> },
|
|
108
|
+
{ id: 'blog-cat', render: ({ id }) => <BlogEntityForm entity={blogCategoryEntity} editId={id!} parentLabel={config.labels.blogCategories} onDone={() => navigate('blog')} /> },
|
|
88
109
|
{ id: 'overview', render: () => <OverviewView /> },
|
|
89
110
|
], 'overview')
|
|
90
111
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { EntityDef, FieldDef } from '@fayz-ai/core'
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Blog backoffice entities — the single source of truth that drives BOTH the
|
|
5
|
+
// generic list (CrudListView columns + facets) and the create/edit form
|
|
6
|
+
// (CrudFormPage fields, incl. the markdown body + category relation).
|
|
7
|
+
//
|
|
8
|
+
// Persistence is the generic Supabase provider resolved from `data.table`
|
|
9
|
+
// (plg_blog_posts / plg_blog_categories, owned by @fayz-ai/plugin-blog's
|
|
10
|
+
// migrations). camelCase field keys map to snake_case columns automatically.
|
|
11
|
+
// The website reads the same rows through the anon v_public_blog_posts view.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const BLOG_POSTS_TABLE = 'plg_blog_posts'
|
|
15
|
+
export const BLOG_CATEGORIES_TABLE = 'plg_blog_categories'
|
|
16
|
+
|
|
17
|
+
export interface BlogPostRow {
|
|
18
|
+
id: string
|
|
19
|
+
title: string
|
|
20
|
+
slug: string
|
|
21
|
+
excerpt: string
|
|
22
|
+
body: string
|
|
23
|
+
categoryId?: string
|
|
24
|
+
coverImage?: string
|
|
25
|
+
authorName?: string
|
|
26
|
+
authorRole?: string
|
|
27
|
+
authorAvatarUrl?: string
|
|
28
|
+
authorBio?: string
|
|
29
|
+
status: 'draft' | 'published'
|
|
30
|
+
readTime?: string
|
|
31
|
+
publishedAt?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BlogCategoryRow {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
slug: string
|
|
38
|
+
description?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildBlogPostEntity(): EntityDef<BlogPostRow> {
|
|
42
|
+
const fields: FieldDef[] = [
|
|
43
|
+
{ key: 'title', label: 'Title', type: 'text', required: true, searchable: true, showInTable: true, sortable: true },
|
|
44
|
+
{ key: 'slug', label: 'Slug', type: 'text', required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
|
|
45
|
+
{ key: 'status', label: 'Status', type: 'select', options: ['draft', 'published'], showInTable: true, defaultValue: 'draft' },
|
|
46
|
+
{ key: 'categoryId', label: 'Category', type: 'relation', relation: { table: BLOG_CATEGORIES_TABLE, valueField: 'id', labelField: 'name', tenantScoped: true }, showInTable: false },
|
|
47
|
+
{ key: 'excerpt', label: 'Excerpt', type: 'textarea', span: 2, showInTable: false, placeholder: 'Short summary shown on cards' },
|
|
48
|
+
{ key: 'body', label: 'Body', type: 'markdown', span: 2, showInTable: false },
|
|
49
|
+
{ key: 'coverImage', label: 'Cover image', type: 'image', showInTable: false, placeholder: 'https://…' },
|
|
50
|
+
{ key: 'readTime', label: 'Read time', type: 'text', showInTable: false, placeholder: '5 min' },
|
|
51
|
+
{ key: 'publishedAt', label: 'Published at', type: 'date', showInTable: true, sortable: true },
|
|
52
|
+
{ key: 'authorName', label: 'Author name', type: 'text', showInTable: false },
|
|
53
|
+
{ key: 'authorRole', label: 'Author role', type: 'text', showInTable: false, placeholder: 'e.g. Cirurgião-Dentista' },
|
|
54
|
+
{ key: 'authorAvatarUrl', label: 'Author avatar', type: 'image', showInTable: false, placeholder: 'https://…' },
|
|
55
|
+
{ key: 'authorBio', label: 'Author bio', type: 'textarea', span: 2, showInTable: false },
|
|
56
|
+
]
|
|
57
|
+
return {
|
|
58
|
+
name: 'Post',
|
|
59
|
+
namePlural: 'Posts',
|
|
60
|
+
icon: 'Newspaper',
|
|
61
|
+
fields,
|
|
62
|
+
displayField: 'title',
|
|
63
|
+
subtitleField: 'slug',
|
|
64
|
+
facets: [{ field: 'status', allLabel: 'All' }],
|
|
65
|
+
data: {
|
|
66
|
+
table: BLOG_POSTS_TABLE,
|
|
67
|
+
tenantScoped: true,
|
|
68
|
+
searchColumns: ['title', 'slug', 'excerpt'],
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildBlogCategoryEntity(): EntityDef<BlogCategoryRow> {
|
|
74
|
+
const fields: FieldDef[] = [
|
|
75
|
+
{ key: 'name', label: 'Name', type: 'text', required: true, searchable: true, showInTable: true, sortable: true },
|
|
76
|
+
{ key: 'slug', label: 'Slug', type: 'text', required: true, searchable: true, showInTable: true, hint: 'URL identifier' },
|
|
77
|
+
{ key: 'description', label: 'Description', type: 'textarea', span: 2, showInTable: false },
|
|
78
|
+
]
|
|
79
|
+
return {
|
|
80
|
+
name: 'Category',
|
|
81
|
+
namePlural: 'Categories',
|
|
82
|
+
icon: 'Tags',
|
|
83
|
+
fields,
|
|
84
|
+
displayField: 'name',
|
|
85
|
+
data: {
|
|
86
|
+
table: BLOG_CATEGORIES_TABLE,
|
|
87
|
+
tenantScoped: true,
|
|
88
|
+
searchColumns: ['name', 'slug'],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,8 @@ import { createMockContentPlannerProvider } from './data/contentMock'
|
|
|
22
22
|
import { createSupabaseContentPlannerProvider } from './data/contentSupabase'
|
|
23
23
|
import { createMarketingStore } from './store'
|
|
24
24
|
import { createContentPlannerStore } from './views/content/contentStore'
|
|
25
|
-
import { MIGRATION_000_PLG_RENAME, MIGRATION_001_CONTENT_PLANNER, MIGRATION_002_MULTI_PLATFORM, MIGRATION_003_RECORDING_OPS } from './migrations'
|
|
25
|
+
import { MIGRATION_000_PLG_RENAME, MIGRATION_001_CONTENT_PLANNER, MIGRATION_002_MULTI_PLATFORM, MIGRATION_003_RECORDING_OPS, MIGRATION_004_RANKLAYER } from './migrations'
|
|
26
|
+
import { ranklayerConnectorDef } from './integrations/ranklayer/connectorDef'
|
|
26
27
|
import { DEFAULT_CURRENCY, type MarketingCurrency } from './format'
|
|
27
28
|
import { MARKETING_PRESETS, type MarketingDomain, type MarketingDomainModules } from './presets'
|
|
28
29
|
import type { AcquisitionChannel, ConversionModel } from './types'
|
|
@@ -74,6 +75,8 @@ const DEFAULT_LABELS: MarketingLabels = {
|
|
|
74
75
|
funnel: 'Funnel',
|
|
75
76
|
landingPages: 'Landing pages',
|
|
76
77
|
content: 'Content',
|
|
78
|
+
blog: 'Blog',
|
|
79
|
+
blogCategories: 'Categorias',
|
|
77
80
|
settings: 'Settings',
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -130,6 +133,8 @@ export function createMarketingPlugin(options?: MarketingPluginOptions): PluginM
|
|
|
130
133
|
subtitle: config.labels.pageSubtitle,
|
|
131
134
|
generalSettings: React.createElement(SettingsView),
|
|
132
135
|
registries: marketingRegistries,
|
|
136
|
+
// Enables the "Integrações" tab (renders manifest connectors for this host).
|
|
137
|
+
hostPluginId: 'marketing',
|
|
133
138
|
routeBase: '/settings/marketing',
|
|
134
139
|
}))
|
|
135
140
|
SettingsComponent.displayName = 'MarketingSettings'
|
|
@@ -150,6 +155,7 @@ export function createMarketingPlugin(options?: MarketingPluginOptions): PluginM
|
|
|
150
155
|
{ id: 'marketing', label: config.labels.pageTitle, group: 'Engage' },
|
|
151
156
|
...(config.modules.landingPages ? [{ id: 'marketing.landing-pages', label: config.labels.landingPages, group: 'Engage' }] : []),
|
|
152
157
|
...(config.modules.contentPlanner ? [{ id: 'marketing.content', label: config.labels.content, group: 'Engage' }] : []),
|
|
158
|
+
...(config.modules.blog ? [{ id: 'marketing.blog', label: config.labels.blog, group: 'Engage' }] : []),
|
|
153
159
|
],
|
|
154
160
|
navigation: [
|
|
155
161
|
{
|
|
@@ -204,7 +210,16 @@ export function createMarketingPlugin(options?: MarketingPluginOptions): PluginM
|
|
|
204
210
|
sql: MIGRATION_003_RECORDING_OPS,
|
|
205
211
|
description: 'Recording-day checklist + media asset URL on posts; content-media storage bucket',
|
|
206
212
|
},
|
|
213
|
+
{
|
|
214
|
+
id: 'marketing-004-ranklayer',
|
|
215
|
+
version: '1.3.0',
|
|
216
|
+
sql: MIGRATION_004_RANKLAYER,
|
|
217
|
+
description: 'RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)',
|
|
218
|
+
},
|
|
207
219
|
],
|
|
220
|
+
// RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
|
|
221
|
+
// (control plane only); the real sync lands via an external PR (RANKLAYER.md).
|
|
222
|
+
connectors: [ranklayerConnectorDef],
|
|
208
223
|
aiTools: [
|
|
209
224
|
{
|
|
210
225
|
id: 'marketing.channel-performance',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# RankLayer connector
|
|
2
|
+
|
|
3
|
+
Surfaces in **Marketing → settings → Integrações** (rendered by the shared
|
|
4
|
+
`ConnectorsHub`). Follows the connector contract in
|
|
5
|
+
`packages/core/src/integrations/index.ts` and mirrors the PlugBank precedent
|
|
6
|
+
(`beauty-saas/src/plugins/openbanking`).
|
|
7
|
+
|
|
8
|
+
## Status: SCAFFOLD
|
|
9
|
+
|
|
10
|
+
This ships the control plane only:
|
|
11
|
+
|
|
12
|
+
- `connectorDef.tsx` — the `ConnectorDefinition` (api-key form: API Key + site
|
|
13
|
+
domain, status badge, `ExtraPanel` with a "sync coming soon" note).
|
|
14
|
+
- `data/supabase.ts` — persists the connection row and reads the sync log.
|
|
15
|
+
- `../../migrations/004_ranklayer.sql` — `plg_marketing_ranklayer_integrations`
|
|
16
|
+
+ `plg_marketing_ranklayer_sync_log` (tenant RLS).
|
|
17
|
+
|
|
18
|
+
`testConnection` currently accepts any non-empty API key and **no real sync
|
|
19
|
+
runs** — RankLayer has no public API wired yet.
|
|
20
|
+
|
|
21
|
+
## What the RankLayer PR needs to fill in
|
|
22
|
+
|
|
23
|
+
1. **Edge function** `functions/ranklayer-sync/index.ts` holding the RankLayer
|
|
24
|
+
API key server-side (move it out of the `api_key` column into a secret if the
|
|
25
|
+
provider requires it), with actions:
|
|
26
|
+
- `test_connection` — validate the key against RankLayer.
|
|
27
|
+
- `sync` — the agreed direction. RankLayer generates/publishes SEO pages and
|
|
28
|
+
articles, so the likely first flow is **inbound**: pull RankLayer-published
|
|
29
|
+
articles and upsert them as `plg_blog_posts` (status `published`) so they
|
|
30
|
+
appear on the site's `/blog`. Confirm direction with the RankLayer team.
|
|
31
|
+
2. **Real `testConnection` / `sync`** in `data/supabase.ts` — replace the stubs
|
|
32
|
+
to invoke the edge function; write a `plg_marketing_ranklayer_sync_log` row
|
|
33
|
+
per run (the `SyncRun` audit shape).
|
|
34
|
+
3. **ExtraPanel** — swap the "coming soon" note for a "Sync now" button + the
|
|
35
|
+
real history table (the history rendering is already wired).
|
|
36
|
+
|
|
37
|
+
The connector is already registered on the marketing manifest
|
|
38
|
+
(`connectors: [ranklayerConnectorDef]`) and the Integrações tab is enabled
|
|
39
|
+
(`hostPluginId: 'marketing'` on the settings panel), so no wiring changes are
|
|
40
|
+
needed — only the data-plane implementation above.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// RankLayer as a unified ConnectorDefinition, surfaced in Marketing → settings
|
|
2
|
+
// → Integrações (rendered by the shared ConnectorsHub). SCAFFOLD: the hub gives
|
|
3
|
+
// us the API-key form + Test/Save; the ExtraPanel below explains that automated
|
|
4
|
+
// SEO content sync is being prepared. The real sync lands via an external PR —
|
|
5
|
+
// see ./RANKLAYER.md.
|
|
6
|
+
import React, { useEffect, useState } from 'react'
|
|
7
|
+
import { Sparkles, History, CheckCircle2, AlertCircle } from 'lucide-react'
|
|
8
|
+
import type { ConnectorDefinition } from '@fayz-ai/core'
|
|
9
|
+
import { createRankLayerProvider } from './data/supabase'
|
|
10
|
+
import type { RankLayerIntegration, RankLayerSyncLogEntry } from './types'
|
|
11
|
+
|
|
12
|
+
const provider = createRankLayerProvider()
|
|
13
|
+
|
|
14
|
+
function RankLayerExtraPanel() {
|
|
15
|
+
const [integration, setIntegration] = useState<RankLayerIntegration | null>(null)
|
|
16
|
+
const [log, setLog] = useState<RankLayerSyncLogEntry[]>([])
|
|
17
|
+
const [loaded, setLoaded] = useState(false)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let cancelled = false
|
|
21
|
+
;(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const found = await provider.getIntegration()
|
|
24
|
+
if (cancelled) return
|
|
25
|
+
setIntegration(found)
|
|
26
|
+
if (found) setLog(await provider.getSyncLog(found.id))
|
|
27
|
+
} catch {
|
|
28
|
+
/* not connected / no client — nothing to show */
|
|
29
|
+
} finally {
|
|
30
|
+
if (!cancelled) setLoaded(true)
|
|
31
|
+
}
|
|
32
|
+
})()
|
|
33
|
+
return () => { cancelled = true }
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
if (!loaded || !integration) return null // nothing until the connection is saved
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-4 border-t pt-4">
|
|
40
|
+
<div className="flex items-start gap-3 rounded-md border border-dashed p-3">
|
|
41
|
+
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
42
|
+
<div className="space-y-1">
|
|
43
|
+
<h4 className="text-sm font-semibold">Sincronização de conteúdo — em breve</h4>
|
|
44
|
+
<p className="text-xs text-muted-foreground">
|
|
45
|
+
A publicação automática de páginas e artigos de SEO com a RankLayer está em preparação.
|
|
46
|
+
{integration.siteDomain ? ` Domínio configurado: ${integration.siteDomain}.` : ''} Assim que a
|
|
47
|
+
integração estiver ativa, o histórico de sincronizações aparece aqui.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{log.length > 0 && (
|
|
53
|
+
<div className="space-y-2">
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
<History className="h-4 w-4 text-muted-foreground" />
|
|
56
|
+
<h4 className="text-sm font-semibold">Histórico</h4>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="divide-y rounded-md border text-sm">
|
|
59
|
+
{log.map((r) => (
|
|
60
|
+
<div key={r.id} className="flex items-center gap-3 px-3 py-2">
|
|
61
|
+
<span className="text-xs text-muted-foreground">{new Date(r.createdAt).toLocaleString('pt-BR')}</span>
|
|
62
|
+
<span className="ml-auto text-xs">{r.written}/{r.fetched} publicadas</span>
|
|
63
|
+
{r.status === 'success'
|
|
64
|
+
? <CheckCircle2 className="h-3.5 w-3.5 text-success" />
|
|
65
|
+
: <AlertCircle className="h-3.5 w-3.5 text-warning" />}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const ranklayerConnectorDef: ConnectorDefinition = {
|
|
76
|
+
id: 'ranklayer',
|
|
77
|
+
hostPluginId: 'marketing',
|
|
78
|
+
name: 'RankLayer',
|
|
79
|
+
description: 'Publicação de conteúdo e páginas de SEO por serviço/bairro para ser encontrado no Google e em assistentes de IA.',
|
|
80
|
+
icon: 'Search',
|
|
81
|
+
authKind: 'api-key',
|
|
82
|
+
fields: [
|
|
83
|
+
{ key: 'apiKey', label: 'API Key', type: 'password', placeholder: '••••••••' },
|
|
84
|
+
{ key: 'siteDomain', label: 'Domínio do site', type: 'text', placeholder: 'exemplo.com.br' },
|
|
85
|
+
],
|
|
86
|
+
async getStatus() {
|
|
87
|
+
try {
|
|
88
|
+
const found = await provider.getIntegration()
|
|
89
|
+
return { connected: !!found?.apiKey, detail: found?.siteDomain }
|
|
90
|
+
} catch {
|
|
91
|
+
return { connected: false }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
|
|
95
|
+
saveConnection: async (values) => { await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain }) },
|
|
96
|
+
disconnect: () => provider.disconnect(),
|
|
97
|
+
ExtraPanel: RankLayerExtraPanel,
|
|
98
|
+
}
|