Link pipeline creatives to RRSS calendar pieces with pick-or-create UI.
Agency can choose a scheduled calendar post to prefill title, copy, pillar and date, or create/edit one in the calendar while adding it to the pipeline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
110
src/lib/calendar-documents.ts
Normal file
110
src/lib/calendar-documents.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/** Read/write RRSS calendar pieces stored in documents (type=calendar). */
|
||||
|
||||
import type { RrssPiece } from '@/components/RrssMonthView';
|
||||
import { monthLabel, slugify } from '@/lib/calendar-import';
|
||||
|
||||
export type CalendarDoc = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: unknown;
|
||||
};
|
||||
|
||||
export type CalendarPieceRow = RrssPiece & {
|
||||
month: string;
|
||||
document_id: string;
|
||||
document_title: string;
|
||||
linked_pipeline?: boolean;
|
||||
};
|
||||
|
||||
export function asPlanObject(content: unknown): Record<string, unknown> | null {
|
||||
let c = content;
|
||||
if (typeof c === 'string') {
|
||||
try {
|
||||
c = JSON.parse(c);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!c || typeof c !== 'object') return null;
|
||||
const o = c as Record<string, unknown>;
|
||||
if (o.kind === 'rrss_month' && Array.isArray(o.pieces)) return o;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function piecesFromDocs(docs: CalendarDoc[]): CalendarPieceRow[] {
|
||||
const out: CalendarPieceRow[] = [];
|
||||
for (const doc of docs) {
|
||||
const plan = asPlanObject(doc.content);
|
||||
if (!plan) continue;
|
||||
const month = String(plan.month || '').slice(0, 7);
|
||||
if (!/^\d{4}-\d{2}$/.test(month)) continue;
|
||||
const pieces = plan.pieces as RrssPiece[];
|
||||
for (const p of pieces) {
|
||||
if (!p?.id || !p.title) continue;
|
||||
out.push({
|
||||
...p,
|
||||
month,
|
||||
document_id: doc.id,
|
||||
document_title: doc.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
out.sort((a, b) => {
|
||||
const da = `${a.date} ${a.time || ''}`.localeCompare(`${b.date} ${b.time || ''}`);
|
||||
return da || a.title.localeCompare(b.title);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
export function mapFormatToPipeline(format?: string, kind?: string): string {
|
||||
const hay = `${format || ''} ${kind || ''}`.toLowerCase();
|
||||
if (hay.includes('stor')) return 'story';
|
||||
if (hay.includes('reel')) return 'reel';
|
||||
if (hay.includes('carrusel') || hay.includes('carousel')) return 'carrusel';
|
||||
return 'post';
|
||||
}
|
||||
|
||||
export function mapPipelineFormatToCalendar(format: string): {
|
||||
format: string;
|
||||
kind: string;
|
||||
} {
|
||||
if (format === 'story') return { format: 'Stories', kind: 'story' };
|
||||
if (format === 'reel') return { format: 'Reel', kind: 'reel' };
|
||||
if (format === 'carrusel') return { format: 'Carrusel', kind: 'post' };
|
||||
return { format: 'Post', kind: 'post' };
|
||||
}
|
||||
|
||||
export function buildCaption(piece: Pick<RrssPiece, 'hook' | 'body' | 'cta' | 'hashtags'>): string {
|
||||
return [piece.hook, piece.body, piece.cta, piece.hashtags]
|
||||
.filter((x) => x && String(x).trim())
|
||||
.join('\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function scheduledAtFromPiece(piece: Pick<RrssPiece, 'date' | 'time'>): string | null {
|
||||
const day = (piece.date || '').slice(0, 10);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return null;
|
||||
const time = (piece.time || '10:00').slice(0, 5);
|
||||
const iso = new Date(`${day}T${time}:00`);
|
||||
if (Number.isNaN(iso.getTime())) return `${day}T10:00:00.000Z`;
|
||||
return iso.toISOString();
|
||||
}
|
||||
|
||||
export function newPieceId(date: string, title: string): string {
|
||||
return `${date}-${slugify(title || 'pieza')}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
export function emptyMonthPlan(ym: string, tenantLabel: string) {
|
||||
return {
|
||||
kind: 'rrss_month',
|
||||
month: ym,
|
||||
label: monthLabel(ym),
|
||||
hero: {
|
||||
eyebrow: `4VENTURES × ${tenantLabel.toUpperCase()} · PLAN RRSS`,
|
||||
title: `Calendario editorial · ${monthLabel(ym)}`,
|
||||
lede: 'Piezas del plan editorial.',
|
||||
chips: [],
|
||||
},
|
||||
pieces: [] as RrssPiece[],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user