// Integrations.jsx — Third-party tool connections
(() => {
const { Card, Badge, Switch } = window.TechyFuelOSDesignSystem_be0222;
const STORAGE_KEY = 'tf_integrations';
function readCfg() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; } }
function saveCfg(cfg) { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); }
const INTEGRATIONS = [
{
id: 'slack', name: 'Slack', category: 'Communication',
icon: '💬', color: '#4A154B',
desc: 'Ping a Slack channel when tasks are updated, completed, or overdue.',
fields: [{ key: 'webhookUrl', label: 'Webhook URL', placeholder: 'https://hooks.slack.com/services/...' }],
testable: true,
},
{
id: 'zoom', name: 'Zoom / Google Meet', category: 'Communication',
icon: '📹', color: '#2D8CFF',
desc: 'One-click meeting links from tasks. Paste your Zoom or Meet base URL.',
fields: [
{ key: 'zoomUrl', label: 'Zoom Personal Link', placeholder: 'https://zoom.us/j/your-id' },
{ key: 'meetUrl', label: 'Google Meet Link', placeholder: 'https://meet.google.com/xxx-xxxx-xxx' },
],
},
{
id: 'gcal', name: 'Google Calendar', category: 'Calendar',
icon: '📅', color: '#4285F4',
desc: 'Export all task deadlines as an iCal file to import into any calendar app.',
action: 'export',
},
{
id: 'gmail', name: 'Gmail / Outlook', category: 'Email',
icon: '✉️', color: '#EA4335',
desc: 'Forward emails to your TechyFuel inbox to auto-create tasks.',
fields: [{ key: 'forwardEmail', label: 'Forward-to Email', placeholder: 'tasks@youragency.techyfuel.io', readonly: true }],
},
{
id: 'github', name: 'GitHub / GitLab', category: 'Development',
icon: '🐙', color: '#24292E',
desc: 'Show commit activity linked to tasks using your repo webhook.',
fields: [
{ key: 'githubToken', label: 'Personal Access Token', placeholder: 'ghp_...', type: 'password' },
{ key: 'githubRepo', label: 'Repository (owner/repo)', placeholder: 'techyfuel/my-project' },
],
testable: true,
},
{
id: 'zapier', name: 'Zapier', category: 'Automation',
icon: '⚡', color: '#FF4A00',
desc: 'Connect 1000+ apps via Zapier. Send your Zap webhook URL here.',
fields: [{ key: 'zapierUrl', label: 'Zap Webhook URL', placeholder: 'https://hooks.zapier.com/hooks/catch/...' }],
testable: true,
},
{
id: 'stripe', name: 'Stripe', category: 'Finance',
icon: '💳', color: '#635BFF',
desc: 'Track invoice payments and log Stripe charges per project.',
fields: [{ key: 'stripeKey', label: 'Stripe Secret Key', placeholder: 'sk_live_...', type: 'password' }],
testable: true,
},
{
id: 'figma', name: 'Figma', category: 'Design',
icon: '🎨', color: '#F24E1E',
desc: 'Embed Figma design previews directly inside tasks and documents.',
fields: [{ key: 'figmaToken', label: 'Figma API Token', placeholder: 'figd_...' }],
embedable: true,
},
{
id: 'loom', name: 'Loom', category: 'Design',
icon: '🎬', color: '#625DF5',
desc: 'Attach Loom video recordings to tasks and chat messages as inline previews.',
embedable: true,
},
{
id: 'gdrive', name: 'Google Drive', category: 'File Storage',
icon: '🗂️', color: '#0F9D58',
desc: "Pick real files straight from your Drive using Google's file picker — from the Files screen.",
fields: [
{ key: 'driveClientId', label: 'OAuth Client ID', placeholder: 'xxx.apps.googleusercontent.com' },
{ key: 'driveApiKey', label: 'API Key (for Picker)', placeholder: 'AIza...' },
],
note: 'In Google Cloud Console: enable the "Google Drive API" and "Google Picker API", create an OAuth 2.0 Client ID (Web application — add your site\'s URL under Authorized JavaScript origins), and create an API key restricted to the Picker API. Once saved here, use "Connect Google Drive" on the Files screen.',
},
{
id: 'paypal', name: 'PayPal', category: 'Finance',
icon: '🅿️', color: '#003087',
desc: 'Accept invoice payments and log PayPal transactions.',
fields: [
{ key: 'paypalClientId', label: 'Client ID', placeholder: 'AY...' },
{ key: 'paypalSecret', label: 'Secret', placeholder: '••••••', type: 'password' },
],
note: 'From the PayPal Developer Dashboard → Apps & Credentials, create a REST API app and paste its Client ID and Secret. Live keys enable real invoice payments.',
},
{
id: 'meta', name: 'Meta / Facebook Ads', category: 'Marketing',
icon: '📣', color: '#1877F2',
desc: 'Pull live ad-campaign metrics and (later) publish posts.',
fields: [
{ key: 'metaAccessToken', label: 'Access Token', placeholder: 'EAAB...', type: 'password' },
{ key: 'metaAdAccountId', label: 'Ad Account ID', placeholder: 'act_1234567890' },
],
note: 'From Meta for Developers → your app → generate a long-lived access token with ads_read / ads_management, and copy your Ad Account ID (starts with act_).',
},
{
id: 'openai', name: 'AI Provider (Groq / OpenAI / Gemini)', category: 'Automation',
icon: '🤖', color: '#10A37F',
desc: 'Bring your own AI key. Leave blank to use the built-in assistant.',
fields: [
{ key: 'aiProvider', label: 'Provider (groq / openai / gemini)', placeholder: 'groq' },
{ key: 'aiApiKey', label: 'API Key', placeholder: 'sk-... / gsk_... / AIza...', type: 'password' },
],
note: 'Optional — the app already ships with a working AI assistant. Add your own key only if you want to use a different provider or your own quota.',
},
];
const CATEGORIES = [...new Set(INTEGRATIONS.map(i => i.category))];
// ── Figma Embed Preview ────────────────────────────────────────────────────────
function FigmaViewer({ onClose }) {
const [url, setUrl] = React.useState('');
const [embedUrl, setEmbedUrl] = React.useState('');
function embed() {
if (!url) return;
// Convert share URL to embed URL
const match = url.match(/figma\.com\/(file|proto|design)\/([^/?]+)/);
if (match) {
setEmbedUrl(`https://www.figma.com/embed?embed_host=techyfuel&url=${encodeURIComponent(url)}`);
} else {
setEmbedUrl(url);
}
}
return (
e.stopPropagation()}>
🎨
setUrl(e.target.value)} placeholder="Paste Figma share URL..."
style={{ flex: 1, height: 36, padding: '0 12px', border: '1px solid var(--slate-200)', borderRadius: 8, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none' }}
onKeyDown={e => e.key === 'Enter' && embed()} />
Preview
{embedUrl ? (
) : (
🎨
Paste a Figma URL above and click Preview
Supports File, Prototype, and Design links
)}
);
}
// ── Loom Embed Preview ─────────────────────────────────────────────────────────
function LoomViewer({ onClose }) {
const [url, setUrl] = React.useState('');
const [embedUrl, setEmbedUrl] = React.useState('');
function embed() {
if (!url) return;
const match = url.match(/loom\.com\/share\/([a-f0-9]+)/);
if (match) setEmbedUrl(`https://www.loom.com/embed/${match[1]}`);
else setEmbedUrl(url);
}
return (
e.stopPropagation()}>
🎬
setUrl(e.target.value)} placeholder="Paste Loom share URL..."
style={{ flex: 1, height: 36, padding: '0 12px', border: '1px solid var(--slate-200)', borderRadius: 8, fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none' }}
onKeyDown={e => e.key === 'Enter' && embed()} />
Load
{embedUrl ? (
) : (
🎬
Paste a Loom share link and click Load
)}
);
}
// ── iCal Export ────────────────────────────────────────────────────────────────
async function exportICal() {
let tasks = [];
if (window.API) {
try { const { data } = await window.API.getTasks(); tasks = data || []; } catch {}
}
const lines = [
'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//TechyFuel OS//EN', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH',
];
for (const t of tasks) {
if (!t.due_date) continue;
const dt = t.due_date.replace(/-/g, '');
lines.push('BEGIN:VEVENT');
lines.push(`UID:task-${t.id}@techyfuel.os`);
lines.push(`DTSTART;VALUE=DATE:${dt}`);
lines.push(`DTEND;VALUE=DATE:${dt}`);
lines.push(`SUMMARY:${(t.title || 'Task').replace(/[\\;,]/g, '\\$&')}`);
lines.push(`STATUS:${t.status === 'done' ? 'COMPLETED' : 'NEEDS-ACTION'}`);
lines.push('END:VEVENT');
}
lines.push('END:VCALENDAR');
const blob = new Blob([lines.join('\r\n')], { type: 'text/calendar' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'techyfuel-tasks.ics';
a.click();
}
// ── Integration Card ───────────────────────────────────────────────────────────
function IntegrationCard({ integ, cfg, onChange }) {
const connected = !!(integ.fields?.some(f => cfg[f.key]) || integ.action);
const [open, setOpen] = React.useState(false);
const [local, setLocal] = React.useState(() => { const o = {}; (integ.fields || []).forEach(f => { o[f.key] = cfg[f.key] || ''; }); return o; });
const [testing, setTesting] = React.useState(false);
const [testResult, setTestResult] = React.useState(null);
const [figmaOpen, setFigmaOpen] = React.useState(false);
const [loomOpen, setLoomOpen] = React.useState(false);
function save() {
onChange({ ...cfg, ...local });
setOpen(false);
setTestResult(null);
}
async function test() {
setTesting(true); setTestResult(null);
try {
if (integ.id === 'slack') {
const res = await fetch(local.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: '✅ TechyFuel OS connected successfully!' }) });
setTestResult(res.ok ? 'success' : 'error');
} else if (integ.id === 'zapier') {
const res = await fetch(local.zapierUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source: 'TechyFuel OS', test: true, timestamp: new Date().toISOString() }) });
setTestResult(res.ok ? 'success' : 'error');
} else if (integ.id === 'stripe') {
const res = await fetch('https://api.stripe.com/v1/balance', { headers: { Authorization: `Bearer ${local.stripeKey}` } });
setTestResult(res.ok ? 'success' : 'error');
} else if (integ.id === 'github') {
const res = await fetch(`https://api.github.com/repos/${local.githubRepo}/commits?per_page=1`, { headers: { Authorization: `token ${local.githubToken}`, Accept: 'application/vnd.github.v3+json' } });
setTestResult(res.ok ? 'success' : 'error');
} else {
setTestResult('success');
}
} catch { setTestResult('error'); }
setTesting(false);
}
const isConnected = integ.fields?.some(f => !f.readonly && cfg[f.key]);
return (
<>
e.currentTarget.style.boxShadow = 'var(--shadow-md)'}
onMouseLeave={e => e.currentTarget.style.boxShadow = 'none'}>
{integ.icon}
{integ.name}
{isConnected && Connected }
{integ.desc}
{integ.action === 'export' && (
Export .ics
)}
{integ.embedable && integ.id === 'figma' && (
setFigmaOpen(true)} style={{ flex: 1, padding: '8px 0', background: integ.color, color: 'white', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 13, fontWeight: 700, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
Open Figma Viewer
)}
{integ.embedable && integ.id === 'loom' && (
setLoomOpen(true)} style={{ flex: 1, padding: '8px 0', background: integ.color, color: 'white', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 13, fontWeight: 700, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
Open Loom Player
)}
{integ.fields && (
setOpen(o => !o)} style={{ flex: 1, padding: '8px 0', background: open ? 'var(--slate-100)' : isConnected ? 'var(--slate-50)' : integ.color, color: open ? 'var(--text-body)' : isConnected ? 'var(--text-body)' : 'white', border: `1px solid ${isConnected || open ? 'var(--slate-200)' : 'transparent'}`, borderRadius: 8, cursor: 'pointer', fontSize: 13, fontWeight: 700, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
{isConnected ? 'Configure' : 'Connect'}
)}
{integ.id === 'zoom' && cfg.zoomUrl && (
window.open(cfg.zoomUrl, '_blank')} style={{ padding: '8px 14px', background: integ.color, color: 'white', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 13, fontWeight: 700, fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', gap: 6 }}>
Start
)}
{/* Config panel */}
{open && integ.fields && (
{integ.fields.map(f => (
{f.label}
!f.readonly && setLocal(l => ({ ...l, [f.key]: e.target.value }))}
type={f.type || 'text'}
placeholder={f.placeholder}
readOnly={f.readonly}
style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--slate-200)', borderRadius: 8, fontSize: 13, fontFamily: 'var(--font-sans)', outline: 'none', background: f.readonly ? 'var(--slate-100)' : 'white', boxSizing: 'border-box', color: f.readonly ? 'var(--text-muted)' : 'inherit' }}
/>
))}
{integ.note && (
{integ.note}
)}
{testResult && (
{testResult === 'success' ? '✅ Connection successful!' : '❌ Connection failed. Check your credentials.'}
)}
Save
{integ.testable && {testing ? 'Testing...' : 'Test'} }
{isConnected && { const n = {}; integ.fields.forEach(f => { n[f.key] = ''; }); onChange({ ...cfg, ...n }); setLocal({}); setOpen(false); }} style={{ padding: '9px 14px', background: 'none', color: 'var(--red-500)', border: '1px solid var(--red-200)', borderRadius: 8, cursor: 'pointer', fontSize: 13, fontFamily: 'var(--font-sans)' }}>Disconnect }
)}
{figmaOpen && setFigmaOpen(false)} />}
{loomOpen && setLoomOpen(false)} />}
>
);
}
// ── Main Screen ────────────────────────────────────────────────────────────────
function Integrations() {
useLucide();
const [cfg, setCfg] = React.useState(readCfg);
const [filter, setFilter] = React.useState('All');
function handleChange(id, updated) {
const next = { ...cfg, [id]: updated };
setCfg(next);
saveCfg(next);
}
const cats = ['All', ...CATEGORIES];
const visible = filter === 'All' ? INTEGRATIONS : INTEGRATIONS.filter(i => i.category === filter);
const connectedCount = INTEGRATIONS.filter(i => i.fields?.some(f => !f.readonly && cfg[i.id]?.[f.key])).length;
return (
{/* Header */}
Integrations
Connect your favorite tools. {connectedCount > 0 && {connectedCount} active }
{/* Category filter */}
{cats.map(cat => (
setFilter(cat)}
style={{ padding: '6px 16px', borderRadius: 20, border: '1px solid', fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--font-sans)', borderColor: filter === cat ? 'var(--blue-500)' : 'var(--slate-200)', background: filter === cat ? 'var(--blue-50)' : 'white', color: filter === cat ? 'var(--blue-700)' : 'var(--text-body)' }}>
{cat}
))}
{/* Grid */}
{visible.map(integ => (
handleChange(integ.id, updated)} />
))}
{/* Info box */}
Custom integrations via Webhooks
Need something not listed? Use the Automations → Webhooks tab to connect any tool with a custom HTTP endpoint. Works with Make, n8n, Pipedream, and any custom API.
);
}
Object.assign(window, { Integrations });
})();