163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import { Plus, X, CheckCircle2, XCircle, Clock, Zap } from "lucide-react";
|
|
import { useCampaignStore } from "@/lib/stores/campaign-store";
|
|
import type { PostData, Platform, PostPlatformContent, PostSyncState, PostMetrics } from "@/lib/types/campaign";
|
|
import { PLATFORMS } from "@/lib/types/campaign";
|
|
|
|
interface Props {
|
|
nodeId: string;
|
|
data: PostData;
|
|
}
|
|
|
|
function SyncBadge({ state }: { state: PostSyncState }) {
|
|
const icons: Record<string, React.ReactNode> = {
|
|
synced: <CheckCircle2 className="w-3 h-3 text-emerald-500" />,
|
|
failed: <XCircle className="w-3 h-3 text-red-500" />,
|
|
pending: <Clock className="w-3 h-3 text-amber-500" />,
|
|
published: <Zap className="w-3 h-3 text-blue-500" />,
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-1 text-xs">
|
|
{icons[state.status]}
|
|
<span
|
|
className={
|
|
state.status === "synced"
|
|
? "text-emerald-500"
|
|
: state.status === "failed"
|
|
? "text-red-500"
|
|
: state.status === "published"
|
|
? "text-blue-500"
|
|
: "text-amber-500"
|
|
}
|
|
>
|
|
{state.status}
|
|
</span>
|
|
{state.error && (
|
|
<span className="text-red-400 truncate max-w-[150px]" title={state.error}>
|
|
{state.error}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricsBadge({ metrics }: { metrics: PostMetrics }) {
|
|
const parts: string[] = [];
|
|
if (metrics.likes != null) parts.push(`${metrics.likes} likes`);
|
|
if (metrics.comments != null) parts.push(`${metrics.comments} comments`);
|
|
if (metrics.shares != null) parts.push(`${metrics.shares} shares`);
|
|
if (metrics.impressions != null) parts.push(`${metrics.impressions} impr.`);
|
|
|
|
if (parts.length === 0) return null;
|
|
|
|
return (
|
|
<div className="text-xs text-muted-foreground bg-muted/50 rounded px-2 py-0.5 mt-1">
|
|
{parts.join(" · ")}
|
|
{metrics.fetchedAt && (
|
|
<span className="ml-1 opacity-50">
|
|
({new Date(metrics.fetchedAt).toLocaleTimeString()})
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PostForm({ nodeId, data }: Props) {
|
|
const updateNodeData = useCampaignStore((s) => s.updateNodeData);
|
|
|
|
const addPlatform = (platformId: Platform) => {
|
|
if (data.platforms.some((p) => p.platform === platformId)) return;
|
|
const newPlatforms: PostPlatformContent[] = [
|
|
...data.platforms,
|
|
{ platform: platformId, content: data.sharedContent },
|
|
];
|
|
updateNodeData(nodeId, { platforms: newPlatforms });
|
|
};
|
|
|
|
const removePlatform = (platformId: Platform) => {
|
|
updateNodeData(nodeId, {
|
|
platforms: data.platforms.filter((p) => p.platform !== platformId),
|
|
});
|
|
};
|
|
|
|
const updatePlatformContent = (platformId: Platform, content: string) => {
|
|
updateNodeData(nodeId, {
|
|
platforms: data.platforms.map((p) =>
|
|
p.platform === platformId ? { ...p, content } : p
|
|
),
|
|
});
|
|
};
|
|
|
|
const availablePlatforms = PLATFORMS.filter(
|
|
(p) => !data.platforms.some((dp) => dp.platform === p.id)
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1.5">Shared Content</label>
|
|
<textarea
|
|
value={data.sharedContent}
|
|
onChange={(e) => updateNodeData(nodeId, { sharedContent: e.target.value })}
|
|
placeholder="Write your post content..."
|
|
rows={3}
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1.5">Platforms</label>
|
|
{data.platforms.map((pp) => {
|
|
const info = PLATFORMS.find((p) => p.id === pp.platform);
|
|
const syncState = data.postizSync?.[pp.platform];
|
|
const platformMetrics = data.metrics?.[pp.platform];
|
|
|
|
return (
|
|
<div key={pp.platform} className="mb-3 rounded-lg border border-border p-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium" style={{ color: info?.color }}>
|
|
{info?.label}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
{syncState && <SyncBadge state={syncState} />}
|
|
<button
|
|
onClick={() => removePlatform(pp.platform)}
|
|
className="text-muted-foreground hover:text-destructive"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<textarea
|
|
value={pp.content}
|
|
onChange={(e) => updatePlatformContent(pp.platform, e.target.value)}
|
|
placeholder={`Custom content for ${info?.label}...`}
|
|
rows={2}
|
|
className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground resize-none focus:outline-none focus:ring-1 focus:ring-primary/50"
|
|
/>
|
|
{platformMetrics && <MetricsBadge metrics={platformMetrics} />}
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{availablePlatforms.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
{availablePlatforms.map((p) => (
|
|
<button
|
|
key={p.id}
|
|
onClick={() => addPlatform(p.id)}
|
|
className="inline-flex items-center gap-1 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground hover:border-primary hover:text-primary transition-colors"
|
|
>
|
|
<Plus className="w-3 h-3" />
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|