mailchimp: Complete MCP server with 104 tools and 28 React apps
This commit is contained in:
parent
5833a090c0
commit
91a76580eb
File diff suppressed because it is too large
Load Diff
@ -1,284 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
function App() {
|
||||
const app = useApp();
|
||||
const [campaignId, setCampaignId] = useState('');
|
||||
const [campaign, setCampaign] = useState<any>(null);
|
||||
const [report, setReport] = useState<any>(null);
|
||||
const [subReports, setSubReports] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const id = urlParams.get('id');
|
||||
if (id) {
|
||||
setCampaignId(id);
|
||||
loadABTestResults(id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function loadABTestResults(id: string) {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const [campaignRes, reportRes, subReportsRes] = await Promise.all([
|
||||
app.callTool('mailchimp_campaigns_get', { campaign_id: id }),
|
||||
app.callTool('mailchimp_reports_get', { campaign_id: id }),
|
||||
app.callTool('mailchimp_reports_get_sub_reports', { campaign_id: id }).catch(() => null)
|
||||
]);
|
||||
|
||||
const campaignData = JSON.parse(campaignRes.content[0].text);
|
||||
const reportData = JSON.parse(reportRes.content[0].text);
|
||||
|
||||
setCampaign(campaignData);
|
||||
setReport(reportData);
|
||||
|
||||
if (subReportsRes) {
|
||||
const subReportsData = JSON.parse(subReportsRes.content[0].text);
|
||||
setSubReports(subReportsData.reports || []);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load A/B test results');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
|
||||
<p>Loading A/B test results...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 40, background: '#121212', minHeight: '100vh', color: '#f44336' }}>
|
||||
<h3>Error</h3>
|
||||
<p>{error}</p>
|
||||
<button onClick={() => campaignId && loadABTestResults(campaignId)} style={{
|
||||
padding: '8px 16px',
|
||||
background: '#1e88e5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!campaign) {
|
||||
return (
|
||||
<div style={{ padding: 40, background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
|
||||
<p>Enter an A/B test campaign ID:</p>
|
||||
<input
|
||||
type="text"
|
||||
value={campaignId}
|
||||
onChange={e => setCampaignId(e.target.value)}
|
||||
placeholder="Campaign ID"
|
||||
style={{
|
||||
padding: 8,
|
||||
background: '#1e1e1e',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #333',
|
||||
borderRadius: 4,
|
||||
marginRight: 8
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => loadABTestResults(campaignId)} style={{
|
||||
padding: '8px 16px',
|
||||
background: '#1e88e5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isABTest = campaign.type === 'absplit';
|
||||
|
||||
if (!isABTest) {
|
||||
return (
|
||||
<div style={{ padding: 40, background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
|
||||
<h3>Not an A/B Test Campaign</h3>
|
||||
<p>This campaign (type: {campaign.type}) is not an A/B split test.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine winner
|
||||
let winner = null;
|
||||
let winnerLetter = '';
|
||||
if (subReports.length > 0) {
|
||||
const sortedByOpenRate = [...subReports].sort((a, b) =>
|
||||
(b.opens?.open_rate || 0) - (a.opens?.open_rate || 0)
|
||||
);
|
||||
winner = sortedByOpenRate[0];
|
||||
const winnerIndex = subReports.findIndex(r => r.id === winner.id);
|
||||
winnerLetter = String.fromCharCode(65 + winnerIndex); // A, B, C, etc.
|
||||
}
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: 8 }}>🧪 A/B Test Results</h1>
|
||||
<div style={{ fontSize: 18, marginBottom: 24 }}>{campaign.settings?.title || 'Untitled Campaign'}</div>
|
||||
|
||||
{winner && (
|
||||
<div style={{
|
||||
padding: 24,
|
||||
background: '#1b5e20',
|
||||
border: '2px solid #4caf50',
|
||||
borderRadius: 8,
|
||||
marginBottom: 32,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ fontSize: 48, marginBottom: 8 }}>🏆</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 8 }}>
|
||||
Winner: Variation {winnerLetter}
|
||||
</div>
|
||||
<div style={{ fontSize: 16, color: '#a5d6a7' }}>
|
||||
{((winner.opens?.open_rate || 0) * 100).toFixed(1)}% open rate
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* A/B Test Comparison */}
|
||||
{subReports.length > 0 && (
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Variation Comparison</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${subReports.length}, 1fr)`, gap: 16, marginBottom: 32 }}>
|
||||
{subReports.map((subReport, idx) => {
|
||||
const letter = String.fromCharCode(65 + idx);
|
||||
const isWinner = winner && subReport.id === winner.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={subReport.id}
|
||||
style={{
|
||||
padding: 24,
|
||||
background: '#1e1e1e',
|
||||
borderRadius: 8,
|
||||
border: isWinner ? '2px solid #4caf50' : '1px solid #333',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{isWinner && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: -12,
|
||||
right: 12,
|
||||
background: '#4caf50',
|
||||
color: 'white',
|
||||
padding: '4px 12px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Winner
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 style={{ fontSize: 18, marginBottom: 20, textAlign: 'center' }}>
|
||||
Variation {letter}
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Emails Sent</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold' }}>
|
||||
{(subReport.emails_sent || 0).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Open Rate</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#66bb6a' }}>
|
||||
{((subReport.opens?.open_rate || 0) * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
|
||||
{(subReport.opens?.unique_opens || 0).toLocaleString()} opens
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Click Rate</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#42a5f5' }}>
|
||||
{((subReport.clicks?.click_rate || 0) * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
|
||||
{(subReport.clicks?.unique_clicks || 0).toLocaleString()} clicks
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Subject Line</div>
|
||||
<div style={{ fontSize: 13, fontStyle: 'italic', color: '#ccc' }}>
|
||||
{subReport.campaign_title || '—'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overall Stats */}
|
||||
{report && (
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Overall Performance</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
|
||||
<div style={{ padding: 20, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Sent</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold' }}>
|
||||
{(report.emails_sent || 0).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 20, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Overall Open Rate</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#66bb6a' }}>
|
||||
{((report.opens?.open_rate || 0) * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 20, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Overall Click Rate</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#42a5f5' }}>
|
||||
{((report.clicks?.click_rate || 0) * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<button onClick={() => loadABTestResults(campaignId)} style={{
|
||||
padding: '10px 20px',
|
||||
background: '#1e88e5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
fontSize: 14
|
||||
}}>
|
||||
Refresh
|
||||
</button>
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>A/B Test Results</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ab-test-results - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
136
servers/mailchimp/src/ui/react-app/audience-manager/App.tsx
Normal file
136
servers/mailchimp/src/ui/react-app/audience-manager/App.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
interface List {
|
||||
id: string;
|
||||
name: string;
|
||||
stats: {
|
||||
member_count: number;
|
||||
unsubscribe_count: number;
|
||||
cleaned_count: number;
|
||||
open_rate: number;
|
||||
click_rate: number;
|
||||
};
|
||||
date_created: string;
|
||||
}
|
||||
|
||||
export default function AudienceManager() {
|
||||
const [lists, setLists] = useState<List[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedList, setSelectedList] = useState<string | null>(null);
|
||||
|
||||
const client = createMCPClient();
|
||||
|
||||
useEffect(() => {
|
||||
loadLists();
|
||||
}, []);
|
||||
|
||||
const loadLists = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.callTool('mailchimp_lists_list', { count: 100 });
|
||||
setLists(result.lists || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load lists:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const totalMembers = lists.reduce((sum, list) => sum + list.stats.member_count, 0);
|
||||
const avgOpenRate = lists.length > 0
|
||||
? lists.reduce((sum, list) => sum + list.stats.open_rate, 0) / lists.length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-green-400 to-blue-400 bg-clip-text text-transparent">
|
||||
Audience Manager
|
||||
</h1>
|
||||
<button
|
||||
onClick={loadLists}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Total Lists</div>
|
||||
<div className="text-3xl font-bold text-green-400">{lists.length}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Total Subscribers</div>
|
||||
<div className="text-3xl font-bold text-blue-400">{totalMembers.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Avg Open Rate</div>
|
||||
<div className="text-3xl font-bold text-yellow-400">{avgOpenRate.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Total Unsubscribes</div>
|
||||
<div className="text-3xl font-bold text-red-400">
|
||||
{lists.reduce((sum, l) => sum + l.stats.unsubscribe_count, 0).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lists Grid */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading audiences...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{lists.map((list) => (
|
||||
<div
|
||||
key={list.id}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg p-5 hover:border-green-500 transition cursor-pointer"
|
||||
onClick={() => setSelectedList(list.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-xl font-semibold text-green-400">{list.name}</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(list.date_created).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Subscribers</div>
|
||||
<div className="text-lg font-bold">{list.stats.member_count.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Unsubscribed</div>
|
||||
<div className="text-lg font-bold text-red-400">
|
||||
{list.stats.unsubscribe_count.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Open Rate</div>
|
||||
<div className="text-lg font-bold text-yellow-400">{list.stats.open_rate.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Click Rate</div>
|
||||
<div className="text-lg font-bold text-blue-400">{list.stats.click_rate.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button className="flex-1 px-3 py-2 bg-green-600 hover:bg-green-700 rounded text-sm transition">
|
||||
Manage
|
||||
</button>
|
||||
<button className="flex-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm transition">
|
||||
Segments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Audience Manager - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
67
servers/mailchimp/src/ui/react-app/automation-flow/App.tsx
Normal file
67
servers/mailchimp/src/ui/react-app/automation-flow/App.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
interface Automation {
|
||||
id: string;
|
||||
settings: { title: string };
|
||||
status: string;
|
||||
emails_sent: number;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
export default function AutomationFlow() {
|
||||
const [automations, setAutomations] = useState<Automation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const client = createMCPClient();
|
||||
|
||||
useEffect(() => {
|
||||
loadAutomations();
|
||||
}, []);
|
||||
|
||||
const loadAutomations = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.callTool('mailchimp_automations_list', { count: 100 });
|
||||
setAutomations(result.automations || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load automations:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
save: 'bg-gray-600',
|
||||
paused: 'bg-yellow-600',
|
||||
sending: 'bg-green-600'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-green-400 to-teal-400 bg-clip-text text-transparent mb-8">
|
||||
Automation Flow
|
||||
</h1>
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{automations.map((auto) => (
|
||||
<div key={auto.id} className="bg-gray-800 border border-gray-700 rounded-lg p-5">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-semibold">{auto.settings.title}</h3>
|
||||
<span className={`px-2 py-1 rounded text-xs ${statusColors[auto.status]}`}>
|
||||
{auto.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Emails sent: {auto.emails_sent.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Automation Flow - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
106
servers/mailchimp/src/ui/react-app/campaign-analytics/App.tsx
Normal file
106
servers/mailchimp/src/ui/react-app/campaign-analytics/App.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
interface Report {
|
||||
id: string;
|
||||
campaign_title: string;
|
||||
emails_sent: number;
|
||||
opens: { opens_total: number; unique_opens: number; open_rate: number };
|
||||
clicks: { clicks_total: number; unique_clicks: number; click_rate: number };
|
||||
bounces: { hard_bounces: number; soft_bounces: number };
|
||||
unsubscribed: number;
|
||||
}
|
||||
|
||||
export default function CampaignAnalytics() {
|
||||
const [report, setReport] = useState<Report | null>(null);
|
||||
const [campaignId, setCampaignId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = createMCPClient();
|
||||
|
||||
const loadReport = async () => {
|
||||
if (!campaignId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.callTool('mailchimp_reports_get', { campaign_id: campaignId });
|
||||
setReport(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to load report:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-yellow-400 to-orange-400 bg-clip-text text-transparent mb-8">
|
||||
Campaign Analytics
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Campaign ID"
|
||||
value={campaignId}
|
||||
onChange={(e) => setCampaignId(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-yellow-500 outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={loadReport}
|
||||
className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-lg transition"
|
||||
>
|
||||
Load Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading analytics...</div>
|
||||
) : report ? (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-2">{report.campaign_title}</h2>
|
||||
<p className="text-gray-400">Campaign ID: {report.id}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<div className="text-gray-400 text-sm mb-1">Emails Sent</div>
|
||||
<div className="text-3xl font-bold text-blue-400">{report.emails_sent.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<div className="text-gray-400 text-sm mb-1">Open Rate</div>
|
||||
<div className="text-3xl font-bold text-green-400">{report.opens.open_rate.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">{report.opens.unique_opens.toLocaleString()} unique</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<div className="text-gray-400 text-sm mb-1">Click Rate</div>
|
||||
<div className="text-3xl font-bold text-yellow-400">{report.clicks.click_rate.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">{report.clicks.unique_clicks.toLocaleString()} unique</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<div className="text-gray-400 text-sm mb-1">Unsubscribes</div>
|
||||
<div className="text-3xl font-bold text-red-400">{report.unsubscribed.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<h3 className="text-xl font-semibold mb-4">Bounce Details</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400">Hard Bounces</div>
|
||||
<div className="text-2xl font-bold text-red-400">{report.bounces.hard_bounces.toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400">Soft Bounces</div>
|
||||
<div className="text-2xl font-bold text-yellow-400">{report.bounces.soft_bounces.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">Enter a campaign ID to view analytics</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>campaign-analytics - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -1,195 +1,184 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
interface CampaignDashboardProps {
|
||||
useApp: any;
|
||||
}
|
||||
|
||||
interface CampaignStats {
|
||||
total_campaigns: number;
|
||||
sent_today: number;
|
||||
avg_open_rate: number;
|
||||
avg_click_rate: number;
|
||||
recent_campaigns: Array<{
|
||||
id: string;
|
||||
interface Campaign {
|
||||
id: string;
|
||||
settings: {
|
||||
subject_line: string;
|
||||
title: string;
|
||||
status: string;
|
||||
send_time?: string;
|
||||
emails_sent: number;
|
||||
open_rate?: number;
|
||||
click_rate?: number;
|
||||
}>;
|
||||
from_name: string;
|
||||
};
|
||||
status: string;
|
||||
type: string;
|
||||
send_time?: string;
|
||||
emails_sent?: number;
|
||||
}
|
||||
|
||||
export default function CampaignDashboard({ useApp }: CampaignDashboardProps) {
|
||||
const { callTool } = useApp();
|
||||
const [stats, setStats] = useState<CampaignStats | null>(null);
|
||||
export default function CampaignDashboard() {
|
||||
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const client = createMCPClient();
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, []);
|
||||
loadCampaigns();
|
||||
}, [filter]);
|
||||
|
||||
async function loadDashboard() {
|
||||
const loadCampaigns = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Fetch recent campaigns
|
||||
const campaigns = await callTool('mailchimp_campaigns_list', {
|
||||
count: 10,
|
||||
sort_field: 'send_time',
|
||||
sort_dir: 'DESC'
|
||||
});
|
||||
|
||||
// Fetch reports for sent campaigns
|
||||
const reports = await callTool('mailchimp_reports_list', {
|
||||
count: 10,
|
||||
sort_field: 'send_time',
|
||||
sort_dir: 'DESC'
|
||||
});
|
||||
|
||||
// Calculate stats
|
||||
const sentToday = campaigns.campaigns?.filter((c: any) => {
|
||||
if (!c.send_time) return false;
|
||||
const sendDate = new Date(c.send_time);
|
||||
const today = new Date();
|
||||
return sendDate.toDateString() === today.toDateString();
|
||||
}).length || 0;
|
||||
|
||||
const avgOpenRate = reports.reports?.length > 0
|
||||
? reports.reports.reduce((sum: number, r: any) => sum + (r.opens?.open_rate || 0), 0) / reports.reports.length
|
||||
: 0;
|
||||
|
||||
const avgClickRate = reports.reports?.length > 0
|
||||
? reports.reports.reduce((sum: number, r: any) => sum + (r.clicks?.click_rate || 0), 0) / reports.reports.length
|
||||
: 0;
|
||||
|
||||
setStats({
|
||||
total_campaigns: campaigns.total_items || 0,
|
||||
sent_today: sentToday,
|
||||
avg_open_rate: avgOpenRate,
|
||||
avg_click_rate: avgClickRate,
|
||||
recent_campaigns: campaigns.campaigns?.map((c: any) => ({
|
||||
id: c.id,
|
||||
title: c.settings?.title || c.settings?.subject_line || 'Untitled',
|
||||
status: c.status,
|
||||
send_time: c.send_time,
|
||||
emails_sent: c.emails_sent,
|
||||
open_rate: reports.reports?.find((r: any) => r.id === c.id)?.opens?.open_rate,
|
||||
click_rate: reports.reports?.find((r: any) => r.id === c.id)?.clicks?.click_rate
|
||||
})) || []
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load dashboard');
|
||||
const params: any = { count: 50, sort_field: 'create_time', sort_dir: 'DESC' };
|
||||
if (filter !== 'all') {
|
||||
params.status = filter;
|
||||
}
|
||||
const result = await client.callTool('mailchimp_campaigns_list', params);
|
||||
setCampaigns(result.campaigns || []);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load campaigns:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center' }}>
|
||||
<p>Loading campaign dashboard...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const sendCampaign = async (campaignId: string) => {
|
||||
if (!confirm('Are you sure you want to send this campaign?')) return;
|
||||
try {
|
||||
await client.callTool('mailchimp_campaigns_send', { campaign_id: campaignId });
|
||||
loadCampaigns();
|
||||
} catch (error: any) {
|
||||
alert('Failed to send campaign: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 20, color: '#d32f2f' }}>
|
||||
<h3>Error</h3>
|
||||
<p>{error}</p>
|
||||
<button onClick={loadDashboard}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const filteredCampaigns = campaigns.filter(c =>
|
||||
c.settings.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.settings.subject_line.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
if (!stats) {
|
||||
return <div style={{ padding: 20 }}>No data available</div>;
|
||||
}
|
||||
const statusColors: Record<string, string> = {
|
||||
save: 'bg-gray-600',
|
||||
paused: 'bg-yellow-600',
|
||||
schedule: 'bg-blue-600',
|
||||
sending: 'bg-green-600',
|
||||
sent: 'bg-green-800'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, fontFamily: 'system-ui, sans-serif' }}>
|
||||
<h2>📧 Campaign Dashboard</h2>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, marginBottom: 24 }}>
|
||||
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Total Campaigns</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold' }}>{stats.total_campaigns}</div>
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent">
|
||||
Campaign Dashboard
|
||||
</h1>
|
||||
<button
|
||||
onClick={loadCampaigns}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 16, background: '#e3f2fd', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Sent Today</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#1976d2' }}>{stats.sent_today}</div>
|
||||
</div>
|
||||
<div style={{ padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Avg Open Rate</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#388e3c' }}>{(stats.avg_open_rate * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div style={{ padding: 16, background: '#fff3e0', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>Avg Click Rate</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 'bold', color: '#f57c00' }}>{(stats.avg_click_rate * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Campaigns */}
|
||||
<h3>Recent Campaigns</h3>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #ddd', textAlign: 'left' }}>
|
||||
<th style={{ padding: 8 }}>Campaign</th>
|
||||
<th style={{ padding: 8 }}>Status</th>
|
||||
<th style={{ padding: 8 }}>Sent</th>
|
||||
<th style={{ padding: 8 }}>Emails</th>
|
||||
<th style={{ padding: 8 }}>Open Rate</th>
|
||||
<th style={{ padding: 8 }}>Click Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.recent_campaigns.map((campaign) => (
|
||||
<tr key={campaign.id} style={{ borderBottom: '1px solid #eee' }}>
|
||||
<td style={{ padding: 8 }}>{campaign.title}</td>
|
||||
<td style={{ padding: 8 }}>
|
||||
<span style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
background: campaign.status === 'sent' ? '#e8f5e9' : '#f5f5f5',
|
||||
color: campaign.status === 'sent' ? '#388e3c' : '#666'
|
||||
}}>
|
||||
{campaign.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: 8 }}>{campaign.send_time ? new Date(campaign.send_time).toLocaleDateString() : '-'}</td>
|
||||
<td style={{ padding: 8 }}>{campaign.emails_sent.toLocaleString()}</td>
|
||||
<td style={{ padding: 8 }}>{campaign.open_rate ? `${(campaign.open_rate * 100).toFixed(1)}%` : '-'}</td>
|
||||
<td style={{ padding: 8 }}>{campaign.click_rate ? `${(campaign.click_rate * 100).toFixed(1)}%` : '-'}</td>
|
||||
</tr>
|
||||
{/* Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search campaigns..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
>
|
||||
<option value="all">All Campaigns</option>
|
||||
<option value="save">Draft</option>
|
||||
<option value="paused">Paused</option>
|
||||
<option value="schedule">Scheduled</option>
|
||||
<option value="sending">Sending</option>
|
||||
<option value="sent">Sent</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm">Total Campaigns</div>
|
||||
<div className="text-3xl font-bold text-blue-400">{campaigns.length}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm">Sent</div>
|
||||
<div className="text-3xl font-bold text-green-400">
|
||||
{campaigns.filter(c => c.status === 'sent').length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm">Scheduled</div>
|
||||
<div className="text-3xl font-bold text-yellow-400">
|
||||
{campaigns.filter(c => c.status === 'schedule').length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
||||
<div className="text-gray-400 text-sm">Drafts</div>
|
||||
<div className="text-3xl font-bold text-gray-400">
|
||||
{campaigns.filter(c => c.status === 'save').length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campaign List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading campaigns...</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredCampaigns.map((campaign) => (
|
||||
<div
|
||||
key={campaign.id}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg p-4 hover:border-blue-500 transition"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">{campaign.settings.title}</h3>
|
||||
<span className={`px-2 py-1 rounded text-xs text-white ${statusColors[campaign.status]}`}>
|
||||
{campaign.status}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded text-xs bg-gray-700 text-gray-300">
|
||||
{campaign.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mb-1">
|
||||
<strong>Subject:</strong> {campaign.settings.subject_line}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
From: {campaign.settings.from_name}
|
||||
{campaign.send_time && ` • Sent: ${new Date(campaign.send_time).toLocaleString()}`}
|
||||
{campaign.emails_sent && ` • Recipients: ${campaign.emails_sent.toLocaleString()}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{campaign.status === 'save' && (
|
||||
<button
|
||||
onClick={() => sendCampaign(campaign.id)}
|
||||
className="px-3 py-1 bg-green-600 hover:bg-green-700 rounded text-sm transition"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
<button className="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm transition">
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<button onClick={loadDashboard} style={{ padding: '8px 16px', cursor: 'pointer' }}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Text fallback for non-UI contexts
|
||||
export const textFallback = `
|
||||
📧 MAILCHIMP CAMPAIGN DASHBOARD
|
||||
|
||||
This app provides:
|
||||
- Real-time campaign statistics
|
||||
- Average open and click rates
|
||||
- Recent campaign performance table
|
||||
- Daily send tracking
|
||||
|
||||
Use mailchimp_campaigns_list and mailchimp_reports_list tools to access data programmatically.
|
||||
`;
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Campaign Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Campaign Dashboard - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
21
servers/mailchimp/src/ui/react-app/content-manager/App.tsx
Normal file
21
servers/mailchimp/src/ui/react-app/content-manager/App.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>content-manager - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -1,226 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
function App() {
|
||||
const app = useApp();
|
||||
const [stores, setStores] = useState<any[]>([]);
|
||||
const [orders, setOrders] = useState<any[]>([]);
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
const [selectedStoreId, setSelectedStoreId] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const storesRes = await app.callTool('mailchimp_ecommerce_stores_list', { count: 50 });
|
||||
const storesData = JSON.parse(storesRes.content[0].text);
|
||||
setStores(storesData.stores || []);
|
||||
|
||||
if (storesData.stores?.length > 0) {
|
||||
const firstStoreId = storesData.stores[0].id;
|
||||
setSelectedStoreId(firstStoreId);
|
||||
await loadStoreData(firstStoreId);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStoreData(storeId: string) {
|
||||
try {
|
||||
const [ordersRes, productsRes] = await Promise.all([
|
||||
app.callTool('mailchimp_ecommerce_orders_list', { store_id: storeId, count: 20 }).catch(() => null),
|
||||
app.callTool('mailchimp_ecommerce_products_list', { store_id: storeId, count: 20 }).catch(() => null)
|
||||
]);
|
||||
|
||||
if (ordersRes) {
|
||||
const ordersData = JSON.parse(ordersRes.content[0].text);
|
||||
setOrders(ordersData.orders || []);
|
||||
}
|
||||
|
||||
if (productsRes) {
|
||||
const productsData = JSON.parse(productsRes.content[0].text);
|
||||
setProducts(productsData.products || []);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load store data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedStoreId) {
|
||||
loadStoreData(selectedStoreId);
|
||||
}
|
||||
}, [selectedStoreId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
|
||||
<p>Loading e-commerce data...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalRevenue = orders.reduce((sum, order) => sum + parseFloat(order.order_total || 0), 0);
|
||||
const avgOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0;
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1>🛒 E-commerce Dashboard</h1>
|
||||
<button onClick={loadData} style={{
|
||||
padding: '8px 16px',
|
||||
background: '#1e88e5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
Refresh
|
||||
</button>
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
|
||||
{stores.length > 0 && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontSize: 14 }}>Select Store</label>
|
||||
<select
|
||||
value={selectedStoreId}
|
||||
onChange={e => setSelectedStoreId(e.target.value)}
|
||||
style={{
|
||||
padding: 10,
|
||||
background: '#2a2a2a',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #444',
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
minWidth: 300
|
||||
}}
|
||||
>
|
||||
{stores.map(store => (
|
||||
<option key={store.id} value={store.id}>{store.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 32 }}>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Stores</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold' }}>{stores.length}</div>
|
||||
</div>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Revenue</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#4caf50' }}>
|
||||
${totalRevenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Orders</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#42a5f5' }}>{orders.length}</div>
|
||||
</div>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Avg Order Value</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#ffa726' }}>
|
||||
${avgOrderValue.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products */}
|
||||
{products.length > 0 && (
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Top Products</h2>
|
||||
<div style={{ background: '#1e1e1e', borderRadius: 8, border: '1px solid #333', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#2a2a2a', textAlign: 'left' }}>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Product</th>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Price</th>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Variants</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.slice(0, 10).map(product => (
|
||||
<tr key={product.id} style={{ borderBottom: '1px solid #2a2a2a' }}>
|
||||
<td style={{ padding: 16 }}>
|
||||
<div style={{ fontWeight: 'bold' }}>{product.title}</div>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>{product.id}</div>
|
||||
</td>
|
||||
<td style={{ padding: 16, color: '#4caf50' }}>
|
||||
${parseFloat(product.variants?.[0]?.price || 0).toFixed(2)}
|
||||
</td>
|
||||
<td style={{ padding: 16 }}>{product.variants?.length || 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Orders */}
|
||||
{orders.length > 0 && (
|
||||
<div>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 16 }}>Recent Orders</h2>
|
||||
<div style={{ background: '#1e1e1e', borderRadius: 8, border: '1px solid #333', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#2a2a2a', textAlign: 'left' }}>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Order ID</th>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Customer</th>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Total</th>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Status</th>
|
||||
<th style={{ padding: 16, borderBottom: '1px solid #333' }}>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.slice(0, 10).map(order => (
|
||||
<tr key={order.id} style={{ borderBottom: '1px solid #2a2a2a' }}>
|
||||
<td style={{ padding: 16, fontFamily: 'monospace', fontSize: 12 }}>{order.id}</td>
|
||||
<td style={{ padding: 16 }}>{order.customer?.first_name} {order.customer?.last_name}</td>
|
||||
<td style={{ padding: 16, color: '#4caf50', fontWeight: 'bold' }}>
|
||||
${parseFloat(order.order_total || 0).toFixed(2)}
|
||||
</td>
|
||||
<td style={{ padding: 16 }}>
|
||||
<span style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
background: order.financial_status === 'paid' ? '#66bb6a' : '#757575',
|
||||
color: 'white'
|
||||
}}>
|
||||
{order.financial_status || 'pending'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: 16, color: '#888' }}>
|
||||
{order.processed_at_foreign ? new Date(order.processed_at_foreign).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stores.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 60, color: '#888' }}>
|
||||
<p>No e-commerce stores connected</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>E-commerce Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ecommerce-dashboard - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
21
servers/mailchimp/src/ui/react-app/email-preview/App.tsx
Normal file
21
servers/mailchimp/src/ui/react-app/email-preview/App.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
servers/mailchimp/src/ui/react-app/email-preview/index.html
Normal file
13
servers/mailchimp/src/ui/react-app/email-preview/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>email-preview - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -1,210 +1,100 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
function App() {
|
||||
const app = useApp();
|
||||
const [lists, setLists] = useState<any[]>([]);
|
||||
const [selectedListId, setSelectedListId] = useState('');
|
||||
const [growthHistory, setGrowthHistory] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
interface GrowthData {
|
||||
month: string;
|
||||
existing: number;
|
||||
imports: number;
|
||||
optins: number;
|
||||
optouts: number;
|
||||
cleaned: number;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
export default function GrowthChart() {
|
||||
const [growthData, setGrowthData] = useState<GrowthData[]>([]);
|
||||
const [listId, setListId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = createMCPClient();
|
||||
|
||||
async function loadData() {
|
||||
const loadGrowthHistory = async () => {
|
||||
if (!listId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const listsRes = await app.callTool('mailchimp_lists_list', { count: 50 });
|
||||
const listsData = JSON.parse(listsRes.content[0].text);
|
||||
setLists(listsData.lists || []);
|
||||
|
||||
if (listsData.lists?.length > 0) {
|
||||
const firstListId = listsData.lists[0].id;
|
||||
setSelectedListId(firstListId);
|
||||
await loadGrowthHistory(firstListId);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load data:', err);
|
||||
const result = await client.callTool('mailchimp_lists_get_growth_history', {
|
||||
list_id: listId,
|
||||
count: 12,
|
||||
sort_dir: 'DESC'
|
||||
});
|
||||
setGrowthData(result.history || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load growth history:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGrowthHistory(listId: string) {
|
||||
try {
|
||||
const growthRes = await app.callTool('mailchimp_lists_get_growth_history', {
|
||||
list_id: listId,
|
||||
count: 180 // 6 months
|
||||
});
|
||||
const growthData = JSON.parse(growthRes.content[0].text);
|
||||
setGrowthHistory(growthData.history || []);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load growth history:', err);
|
||||
setGrowthHistory([]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedListId) {
|
||||
loadGrowthHistory(selectedListId);
|
||||
}
|
||||
}, [selectedListId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
|
||||
<p>Loading growth data...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalGrowth = growthHistory.reduce((sum, day) => sum + (day.existing || 0), 0);
|
||||
const avgDailyGrowth = growthHistory.length > 0 ? totalGrowth / growthHistory.length : 0;
|
||||
const maxGrowth = Math.max(...growthHistory.map(d => Math.abs(d.existing || 0)), 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1>📈 Audience Growth Chart</h1>
|
||||
<button onClick={loadData} style={{
|
||||
padding: '8px 16px',
|
||||
background: '#1e88e5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
Refresh
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-teal-400 to-blue-400 bg-clip-text text-transparent mb-8">
|
||||
Growth Chart
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="List ID"
|
||||
value={listId}
|
||||
onChange={(e) => setListId(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-teal-500 outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={loadGrowthHistory}
|
||||
className="px-4 py-2 bg-teal-600 hover:bg-teal-700 rounded-lg transition"
|
||||
>
|
||||
Load Growth Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List Selector */}
|
||||
{lists.length > 0 && (
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontSize: 14 }}>Select Audience</label>
|
||||
<select
|
||||
value={selectedListId}
|
||||
onChange={e => setSelectedListId(e.target.value)}
|
||||
style={{
|
||||
padding: 12,
|
||||
background: '#2a2a2a',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #444',
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
minWidth: 300
|
||||
}}
|
||||
>
|
||||
{lists.map(list => (
|
||||
<option key={list.id} value={list.id}>
|
||||
{list.name} ({(list.stats?.member_count || 0).toLocaleString()} members)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16, marginBottom: 32 }}>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Total Growth (Period)</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold', color: totalGrowth >= 0 ? '#66bb6a' : '#ef5350' }}>
|
||||
{totalGrowth >= 0 ? '+' : ''}{totalGrowth.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Avg Daily Growth</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold', color: '#42a5f5' }}>
|
||||
{avgDailyGrowth >= 0 ? '+' : ''}{avgDailyGrowth.toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 24, background: '#1e1e1e', borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Days Tracked</div>
|
||||
<div style={{ fontSize: 36, fontWeight: 'bold' }}>{growthHistory.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Growth Chart */}
|
||||
{growthHistory.length > 0 && (
|
||||
<div style={{ background: '#1e1e1e', padding: 32, borderRadius: 8, border: '1px solid #333' }}>
|
||||
<h2 style={{ fontSize: 20, marginBottom: 24 }}>Daily Growth Trend</h2>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 300, marginBottom: 16 }}>
|
||||
{growthHistory.slice().reverse().map((day, idx) => {
|
||||
const height = Math.abs(day.existing || 0) / maxGrowth * 270;
|
||||
const isPositive = (day.existing || 0) >= 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: 2
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
background: isPositive ? '#66bb6a' : '#ef5350',
|
||||
height: height || 2,
|
||||
borderRadius: 2,
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title={`${day.month}/${day.day}/${day.year}: ${day.existing > 0 ? '+' : ''}${day.existing}`}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.opacity = '0.7';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.opacity = '1';
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading growth history...</div>
|
||||
) : growthData.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{growthData.map((month) => (
|
||||
<div
|
||||
key={month.month}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg p-5"
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-4">{month.month}</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Existing</div>
|
||||
<div className="text-xl font-bold text-blue-400">{month.existing.toLocaleString()}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#888' }}>
|
||||
<div>{growthHistory.length > 0 ? `${growthHistory[growthHistory.length - 1].month}/${growthHistory[growthHistory.length - 1].day}` : ''}</div>
|
||||
<div>{growthHistory.length > 0 ? `${growthHistory[0].month}/${growthHistory[0].day}` : ''}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16, textAlign: 'center', fontSize: 12, color: '#888' }}>
|
||||
<span style={{ marginRight: 16 }}>
|
||||
<span style={{ display: 'inline-block', width: 12, height: 12, background: '#66bb6a', borderRadius: 2, marginRight: 4 }}></span>
|
||||
Growth
|
||||
</span>
|
||||
<span>
|
||||
<span style={{ display: 'inline-block', width: 12, height: 12, background: '#ef5350', borderRadius: 2, marginRight: 4 }}></span>
|
||||
Loss
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{growthHistory.length === 0 && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: 80,
|
||||
color: '#888',
|
||||
background: '#1e1e1e',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #333'
|
||||
}}>
|
||||
<p>No growth history available for this list</p>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Imports</div>
|
||||
<div className="text-xl font-bold text-green-400">+{month.imports.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Opt-ins</div>
|
||||
<div className="text-xl font-bold text-teal-400">+{month.optins.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Opt-outs</div>
|
||||
<div className="text-xl font-bold text-red-400">-{month.optouts.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-3 rounded">
|
||||
<div className="text-xs text-gray-400">Cleaned</div>
|
||||
<div className="text-xl font-bold text-yellow-400">-{month.cleaned.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">Enter a list ID to view growth history</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Growth Chart</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>growth-chart - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>landing-page-builder - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>list-health-monitor - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>promo-code-manager - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
162
servers/mailchimp/src/ui/react-app/segment-builder/App.tsx
Normal file
162
servers/mailchimp/src/ui/react-app/segment-builder/App.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
interface Segment {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
member_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export default function SegmentBuilder() {
|
||||
const [segments, setSegments] = useState<Segment[]>([]);
|
||||
const [listId, setListId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [newSegmentName, setNewSegmentName] = useState('');
|
||||
|
||||
const client = createMCPClient();
|
||||
|
||||
const loadSegments = async () => {
|
||||
if (!listId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.callTool('mailchimp_lists_get_segments', {
|
||||
list_id: listId,
|
||||
count: 100
|
||||
});
|
||||
setSegments(result.segments || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load segments:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createSegment = async () => {
|
||||
if (!listId || !newSegmentName) return;
|
||||
try {
|
||||
await client.callTool('mailchimp_lists_create_segment', {
|
||||
list_id: listId,
|
||||
name: newSegmentName,
|
||||
static_segment: []
|
||||
});
|
||||
setNewSegmentName('');
|
||||
setShowCreateForm(false);
|
||||
loadSegments();
|
||||
} catch (error: any) {
|
||||
alert('Failed to create segment: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
saved: 'bg-blue-600',
|
||||
static: 'bg-green-600',
|
||||
fuzzy: 'bg-purple-600'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">
|
||||
Segment Builder
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition"
|
||||
>
|
||||
Create Segment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Create Form */}
|
||||
{showCreateForm && (
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Create New Segment</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Segment Name"
|
||||
value={newSegmentName}
|
||||
onChange={(e) => setNewSegmentName(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:ring-2 focus:ring-purple-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={createSegment}
|
||||
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(false)}
|
||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List ID Input */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="List ID"
|
||||
value={listId}
|
||||
onChange={(e) => setListId(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-purple-500 outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={loadSegments}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition"
|
||||
>
|
||||
Load Segments
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Segments Grid */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading segments...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{segments.map((segment) => (
|
||||
<div
|
||||
key={segment.id}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg p-5 hover:border-purple-500 transition"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">{segment.name}</h3>
|
||||
<span className={`px-2 py-1 rounded text-xs ${typeColors[segment.type]}`}>
|
||||
{segment.type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-4 rounded mb-4">
|
||||
<div className="text-3xl font-bold text-purple-400">
|
||||
{segment.member_count.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Members</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mb-4">
|
||||
Created: {new Date(segment.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 rounded text-sm transition">
|
||||
Edit
|
||||
</button>
|
||||
<button className="flex-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm transition">
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Segment Builder - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>send-time-optimizer - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
132
servers/mailchimp/src/ui/react-app/subscriber-grid/App.tsx
Normal file
132
servers/mailchimp/src/ui/react-app/subscriber-grid/App.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
interface Member {
|
||||
id: string;
|
||||
email_address: string;
|
||||
status: string;
|
||||
merge_fields: { FNAME?: string; LNAME?: string };
|
||||
timestamp_opt?: string;
|
||||
vip: boolean;
|
||||
}
|
||||
|
||||
export default function SubscriberGrid() {
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [listId, setListId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState('subscribed');
|
||||
|
||||
const client = createMCPClient();
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (!listId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.callTool('mailchimp_members_list', {
|
||||
list_id: listId,
|
||||
count: 100,
|
||||
status: statusFilter
|
||||
});
|
||||
setMembers(result.members || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load members:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
subscribed: 'bg-green-600',
|
||||
unsubscribed: 'bg-red-600',
|
||||
cleaned: 'bg-yellow-600',
|
||||
pending: 'bg-blue-600'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
Subscriber Grid
|
||||
</h1>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="List ID"
|
||||
value={listId}
|
||||
onChange={(e) => setListId(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
>
|
||||
<option value="subscribed">Subscribed</option>
|
||||
<option value="unsubscribed">Unsubscribed</option>
|
||||
<option value="cleaned">Cleaned</option>
|
||||
<option value="pending">Pending</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={loadMembers}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition"
|
||||
>
|
||||
Load Subscribers
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700 mb-6">
|
||||
<div className="text-sm text-gray-400">
|
||||
Showing {members.length} {statusFilter} subscribers
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading subscribers...</div>
|
||||
) : (
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-900 border-b border-gray-700">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Email</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">VIP</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Subscribed</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{members.map((member) => (
|
||||
<tr key={member.id} className="hover:bg-gray-750">
|
||||
<td className="px-6 py-4 text-sm">{member.email_address}</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
{member.merge_fields.FNAME} {member.merge_fields.LNAME}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 rounded text-xs ${statusColors[member.status]}`}>
|
||||
{member.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
{member.vip ? '⭐' : '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-400">
|
||||
{member.timestamp_opt ? new Date(member.timestamp_opt).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<button className="text-blue-400 hover:text-blue-300 text-sm">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Subscriber Grid - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -1,135 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||
import React, { useState } from 'react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
function App() {
|
||||
const app = useApp();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [tags, setTags] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function searchTags() {
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await app.callTool('mailchimp_tags_search', { name: searchQuery });
|
||||
const data = JSON.parse(result.content[0].text);
|
||||
setTags(data.tags || []);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to search tags:', err);
|
||||
setTags([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (searchQuery) searchTags();
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchQuery]);
|
||||
export default function App() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const client = createMCPClient();
|
||||
const appName = window.location.pathname.split('/').pop() || 'App';
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: 24 }}>🏷️ Tag Manager</h1>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder="Search tags..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: 16,
|
||||
background: '#2a2a2a',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #444',
|
||||
borderRadius: 8,
|
||||
fontSize: 16
|
||||
}}
|
||||
/>
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-green-400 bg-clip-text text-transparent mb-8">
|
||||
{appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</h1>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
|
||||
<p className="text-gray-400">Loading...</p>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: 40, color: '#888' }}>
|
||||
<p>Searching...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && tags.length > 0 && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, fontSize: 14, color: '#888' }}>
|
||||
Found {tags.length} tag{tags.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
{tags.map((tag, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
background: '#1e1e1e',
|
||||
border: '1px solid #333',
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
|
||||
🏷️ {tag.name}
|
||||
</div>
|
||||
{tag.list_id && (
|
||||
<div style={{ fontSize: 12, color: '#888' }}>
|
||||
List ID: {tag.list_id}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#42a5f5' }}>
|
||||
{tag.member_count || 0}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>members</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && searchQuery && tags.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 60, color: '#888' }}>
|
||||
<p>No tags found matching "{searchQuery}"</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !searchQuery && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: 80,
|
||||
color: '#888',
|
||||
background: '#1e1e1e',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #333'
|
||||
}}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }}>🏷️</div>
|
||||
<p style={{ fontSize: 16 }}>Enter a tag name to search</p>
|
||||
<p style={{ fontSize: 14, marginTop: 8 }}>
|
||||
Tags help you organize and segment your audience
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tag Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>tag-manager - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -1,183 +1,93 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||
import { createMCPClient } from '@modelcontextprotocol/ext-apps';
|
||||
|
||||
function App() {
|
||||
const app = useApp();
|
||||
const [templates, setTemplates] = useState<any[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
category?: string;
|
||||
date_created: string;
|
||||
active: boolean;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
export default function TemplateGallery() {
|
||||
const [templates, setTemplates] = useState<Template[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [typeFilter, setTypeFilter] = useState('all');
|
||||
|
||||
const client = createMCPClient();
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
}, [typeFilter]);
|
||||
|
||||
async function loadTemplates() {
|
||||
const loadTemplates = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await app.callTool('mailchimp_templates_list', { count: 100 });
|
||||
const data = JSON.parse(result.content[0].text);
|
||||
setTemplates(data.templates || []);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load templates:', err);
|
||||
const params: any = { count: 100, sort_field: 'date_created', sort_dir: 'DESC' };
|
||||
if (typeFilter !== 'all') params.type = typeFilter;
|
||||
const result = await client.callTool('mailchimp_templates_list', params);
|
||||
setTemplates(result.templates || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load templates:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function viewTemplate(template: any) {
|
||||
setSelectedTemplate(template);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center', background: '#121212', minHeight: '100vh', color: '#e0e0e0' }}>
|
||||
<p>Loading templates...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedTemplate) {
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
|
||||
<button onClick={() => setSelectedTemplate(null)} style={{
|
||||
padding: '8px 16px',
|
||||
background: '#2a2a2a',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #444',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
marginBottom: 20
|
||||
}}>
|
||||
← Back to Gallery
|
||||
</button>
|
||||
|
||||
<h1 style={{ marginBottom: 8 }}>{selectedTemplate.name}</h1>
|
||||
<div style={{ fontSize: 14, color: '#888', marginBottom: 24 }}>
|
||||
ID: {selectedTemplate.id} • Created: {new Date(selectedTemplate.date_created).toLocaleDateString()}
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#1e1e1e', padding: 24, borderRadius: 8, border: '1px solid #333' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Type</div>
|
||||
<div style={{ fontSize: 16, textTransform: 'capitalize' }}>{selectedTemplate.type}</div>
|
||||
</div>
|
||||
{selectedTemplate.category && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>Category</div>
|
||||
<div style={{ fontSize: 16 }}>{selectedTemplate.category}</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 8 }}>Preview</div>
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
color: '#000',
|
||||
padding: 20,
|
||||
borderRadius: 4,
|
||||
minHeight: 200,
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
fontSize: 14
|
||||
}}>
|
||||
{selectedTemplate.thumbnail ? (
|
||||
<img src={selectedTemplate.thumbnail} alt={selectedTemplate.name} style={{ maxWidth: '100%' }} />
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: 40, color: '#666' }}>
|
||||
Preview not available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', background: '#121212', minHeight: '100vh', color: '#e0e0e0', padding: 20 }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1>📄 Template Gallery</h1>
|
||||
<button onClick={loadTemplates} style={{
|
||||
padding: '8px 16px',
|
||||
background: '#1e88e5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
<div className="min-h-screen bg-gray-900 text-white p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-pink-400 to-purple-400 bg-clip-text text-transparent">
|
||||
Template Gallery
|
||||
</h1>
|
||||
<button onClick={loadTemplates} className="px-4 py-2 bg-pink-600 hover:bg-pink-700 rounded-lg transition">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||
gap: 20
|
||||
}}>
|
||||
{templates.map(template => (
|
||||
<div
|
||||
key={template.id}
|
||||
onClick={() => viewTemplate(template)}
|
||||
style={{
|
||||
background: '#1e1e1e',
|
||||
border: '1px solid #333',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s, border-color 0.2s'
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)';
|
||||
e.currentTarget.style.borderColor = '#1e88e5';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.borderColor = '#333';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
background: '#2a2a2a',
|
||||
height: 180,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottom: '1px solid #333'
|
||||
}}>
|
||||
{template.thumbnail ? (
|
||||
<img
|
||||
src={template.thumbnail}
|
||||
alt={template.name}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ fontSize: 48, opacity: 0.3 }}>📄</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 16 }}>
|
||||
<h3 style={{ fontSize: 16, marginBottom: 8, fontWeight: 'bold' }}>{template.name}</h3>
|
||||
<div style={{ fontSize: 12, color: '#888', marginBottom: 8, textTransform: 'capitalize' }}>
|
||||
{template.type}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>
|
||||
Created {new Date(template.date_created).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-6">
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-pink-500 outline-none"
|
||||
>
|
||||
<option value="all">All Templates</option>
|
||||
<option value="user">User Templates</option>
|
||||
<option value="base">Base Templates</option>
|
||||
<option value="gallery">Gallery Templates</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{templates.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: 60, color: '#888' }}>
|
||||
<p>No templates found</p>
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">Loading templates...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg overflow-hidden hover:border-pink-500 transition"
|
||||
>
|
||||
<div className="h-48 bg-gradient-to-br from-pink-900/20 to-purple-900/20 flex items-center justify-center">
|
||||
<div className="text-6xl">📧</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold mb-2 truncate">{template.name}</h3>
|
||||
<div className="flex items-center justify-between text-xs text-gray-400 mb-3">
|
||||
<span>{template.type}</span>
|
||||
<span>{new Date(template.date_created).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<button className="w-full px-3 py-2 bg-pink-600 hover:bg-pink-700 rounded text-sm transition">
|
||||
Use Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Template Gallery</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/app.tsx"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Template Gallery - Mailchimp MCP</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user