Add event status trigger, improve test coverage, and UI enhancements
- Add event-status-changed trigger for SmoothSchedule Activepieces piece - Add comprehensive test coverage for payments, tickets, messaging, mobile - Add test coverage for core services, signals, consumers, and views - Improve Activepieces UI: templates, billing hooks, project hooks - Update marketing automation showcase and workflow visual components - Add public API endpoints for availability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
label={t('nav.automations', 'Automations')}
|
||||
isCollapsed={isCollapsed}
|
||||
locked={!canUse('automations')}
|
||||
badgeElement={<UnfinishedBadge />}
|
||||
/>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
@@ -1,197 +1,161 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Mail, Calendar, Bell, ArrowRight, Zap, CheckCircle2, Code, LayoutGrid } from 'lucide-react';
|
||||
import { Mail, Calendar, Bell, ArrowRight, Zap, CheckCircle2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CodeBlock from './CodeBlock';
|
||||
import WorkflowVisual from './WorkflowVisual';
|
||||
|
||||
const AutomationShowcase: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [viewMode, setViewMode] = useState<'marketplace' | 'code'>('marketplace');
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const examples = [
|
||||
{
|
||||
id: 'winback',
|
||||
icon: Mail,
|
||||
title: t('marketing.plugins.examples.winback.title'),
|
||||
description: t('marketing.plugins.examples.winback.description'),
|
||||
stats: [t('marketing.plugins.examples.winback.stats.retention'), t('marketing.plugins.examples.winback.stats.revenue')],
|
||||
marketplaceImage: 'bg-gradient-to-br from-pink-500 to-rose-500',
|
||||
code: t('marketing.plugins.examples.winback.code'),
|
||||
},
|
||||
{
|
||||
id: 'noshow',
|
||||
icon: Bell,
|
||||
title: t('marketing.plugins.examples.noshow.title'),
|
||||
description: t('marketing.plugins.examples.noshow.description'),
|
||||
stats: [t('marketing.plugins.examples.noshow.stats.reduction'), t('marketing.plugins.examples.noshow.stats.utilization')],
|
||||
marketplaceImage: 'bg-gradient-to-br from-blue-500 to-cyan-500',
|
||||
code: t('marketing.plugins.examples.noshow.code'),
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
icon: Calendar,
|
||||
title: t('marketing.plugins.examples.report.title'),
|
||||
description: t('marketing.plugins.examples.report.description'),
|
||||
stats: [t('marketing.plugins.examples.report.stats.timeSaved'), t('marketing.plugins.examples.report.stats.visibility')],
|
||||
marketplaceImage: 'bg-gradient-to-br from-purple-500 to-indigo-500',
|
||||
code: t('marketing.plugins.examples.report.code'),
|
||||
},
|
||||
];
|
||||
const examples = [
|
||||
{
|
||||
id: 'winback',
|
||||
icon: Mail,
|
||||
title: t('marketing.plugins.examples.winback.title'),
|
||||
description: t('marketing.plugins.examples.winback.description'),
|
||||
stats: [
|
||||
t('marketing.plugins.examples.winback.stats.retention'),
|
||||
t('marketing.plugins.examples.winback.stats.revenue'),
|
||||
],
|
||||
variant: 'winback' as const,
|
||||
},
|
||||
{
|
||||
id: 'noshow',
|
||||
icon: Bell,
|
||||
title: t('marketing.plugins.examples.noshow.title'),
|
||||
description: t('marketing.plugins.examples.noshow.description'),
|
||||
stats: [
|
||||
t('marketing.plugins.examples.noshow.stats.reduction'),
|
||||
t('marketing.plugins.examples.noshow.stats.utilization'),
|
||||
],
|
||||
variant: 'noshow' as const,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
icon: Calendar,
|
||||
title: t('marketing.plugins.examples.report.title'),
|
||||
description: t('marketing.plugins.examples.report.description'),
|
||||
stats: [
|
||||
t('marketing.plugins.examples.report.stats.timeSaved'),
|
||||
t('marketing.plugins.examples.report.stats.visibility'),
|
||||
],
|
||||
variant: 'report' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const CurrentIcon = examples[activeTab].icon;
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-gray-50 dark:bg-gray-900 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
|
||||
{/* Left Column: Content */}
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 text-sm font-medium mb-6">
|
||||
<Zap className="w-4 h-4" />
|
||||
<span>{t('marketing.plugins.badge')}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-4xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{t('marketing.plugins.headline')}
|
||||
</h2>
|
||||
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-10">
|
||||
{t('marketing.plugins.subheadline')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{examples.map((example, index) => (
|
||||
<button
|
||||
key={example.id}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={`w-full text-left p-4 rounded-xl transition-all duration-200 border ${activeTab === index
|
||||
? 'bg-white dark:bg-gray-800 border-brand-500 shadow-lg scale-[1.02]'
|
||||
: 'bg-transparent border-transparent hover:bg-white/50 dark:hover:bg-gray-800/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-lg ${activeTab === index
|
||||
? 'bg-brand-100 text-brand-600 dark:bg-brand-900/50 dark:text-brand-400'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
|
||||
}`}>
|
||||
<example.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-semibold mb-1 ${activeTab === index ? 'text-gray-900 dark:text-white' : 'text-gray-600 dark:text-gray-400'
|
||||
}`}>
|
||||
{example.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-500">
|
||||
{example.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Visuals */}
|
||||
<div className="relative">
|
||||
{/* Background Decor */}
|
||||
<div className="absolute -inset-4 bg-gradient-to-r from-brand-500/20 to-purple-500/20 rounded-3xl blur-2xl opacity-50" />
|
||||
|
||||
{/* View Toggle */}
|
||||
<div className="absolute -top-12 right-0 flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setViewMode('marketplace')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all ${viewMode === 'marketplace'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
{t('marketing.plugins.viewToggle.marketplace')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('code')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all ${viewMode === 'code'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Code className="w-4 h-4" />
|
||||
{t('marketing.plugins.viewToggle.developer')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={`${activeTab}-${viewMode}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative mt-8" // Added margin top for toggle
|
||||
>
|
||||
{/* Stats Cards */}
|
||||
<div className="flex gap-4 mb-6">
|
||||
{examples[activeTab].stats.map((stat, i) => (
|
||||
<div key={i} className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{stat}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{viewMode === 'marketplace' ? (
|
||||
// Marketplace Card View
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-xl overflow-hidden">
|
||||
<div className={`h-32 ${examples[activeTab].marketplaceImage} flex items-center justify-center`}>
|
||||
<CurrentIcon className="w-16 h-16 text-white opacity-90" />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white">{examples[activeTab].title}</h3>
|
||||
<div className="text-sm text-gray-500">{t('marketing.plugins.marketplaceCard.author')}</div>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-brand-600 text-white rounded-lg font-medium text-sm hover:bg-brand-700 transition-colors">
|
||||
{t('marketing.plugins.marketplaceCard.installButton')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||
{examples[activeTab].description}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<div className="flex -space-x-2">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="w-6 h-6 rounded-full bg-gray-300 border-2 border-white dark:border-gray-800" />
|
||||
))}
|
||||
</div>
|
||||
<span>{t('marketing.plugins.marketplaceCard.usedBy')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Code View
|
||||
<CodeBlock
|
||||
code={examples[activeTab].code}
|
||||
filename={`${examples[activeTab].id}_automation.py`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<div className="mt-6 text-right">
|
||||
<a href="/features" className="inline-flex items-center gap-2 text-brand-600 dark:text-brand-400 font-medium hover:underline">
|
||||
{t('marketing.plugins.cta')} <ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
return (
|
||||
<section className="py-24 bg-gray-50 dark:bg-gray-900 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Left Column: Content */}
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 text-sm font-medium mb-6">
|
||||
<Zap className="w-4 h-4" />
|
||||
<span>{t('marketing.plugins.badge')}</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
<h2 className="text-4xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{t('marketing.plugins.headline')}
|
||||
</h2>
|
||||
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-10">
|
||||
{t('marketing.plugins.subheadline')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{examples.map((example, index) => (
|
||||
<button
|
||||
key={example.id}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={`w-full text-left p-4 rounded-xl transition-all duration-200 border ${
|
||||
activeTab === index
|
||||
? 'bg-white dark:bg-gray-800 border-brand-500 shadow-lg scale-[1.02]'
|
||||
: 'bg-transparent border-transparent hover:bg-white/50 dark:hover:bg-gray-800/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`p-2 rounded-lg ${
|
||||
activeTab === index
|
||||
? 'bg-brand-100 text-brand-600 dark:bg-brand-900/50 dark:text-brand-400'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<example.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
className={`font-semibold mb-1 ${
|
||||
activeTab === index
|
||||
? 'text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{example.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-500">
|
||||
{example.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Workflow Visual */}
|
||||
<div className="relative">
|
||||
{/* Background Decor */}
|
||||
<div className="absolute -inset-4 bg-gradient-to-r from-brand-500/20 to-purple-500/20 rounded-3xl blur-2xl opacity-50" />
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative"
|
||||
>
|
||||
{/* Stats Cards */}
|
||||
<div className="flex gap-4 mb-6">
|
||||
{examples[activeTab].stats.map((stat, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{stat}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Workflow Visual */}
|
||||
<WorkflowVisual
|
||||
variant={examples[activeTab].variant}
|
||||
trigger={examples[activeTab].title}
|
||||
actions={[]}
|
||||
/>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="mt-6 text-right">
|
||||
<a
|
||||
href="/features"
|
||||
className="inline-flex items-center gap-2 text-brand-600 dark:text-brand-400 font-medium hover:underline"
|
||||
>
|
||||
{t('marketing.plugins.cta')} <ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationShowcase;
|
||||
|
||||
171
frontend/src/components/marketing/WorkflowVisual.tsx
Normal file
171
frontend/src/components/marketing/WorkflowVisual.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Calendar,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
Search,
|
||||
FileText,
|
||||
Sparkles,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface WorkflowBlock {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
type: 'trigger' | 'action';
|
||||
}
|
||||
|
||||
interface WorkflowVisualProps {
|
||||
trigger: string;
|
||||
actions: string[];
|
||||
variant?: 'winback' | 'noshow' | 'report';
|
||||
}
|
||||
|
||||
const getWorkflowConfig = (
|
||||
variant: WorkflowVisualProps['variant']
|
||||
): WorkflowBlock[] => {
|
||||
switch (variant) {
|
||||
case 'winback':
|
||||
return [
|
||||
{ icon: Clock, label: 'Schedule: Weekly', type: 'trigger' },
|
||||
{ icon: Search, label: 'Find Inactive Customers', type: 'action' },
|
||||
{ icon: Mail, label: 'Send Email', type: 'action' },
|
||||
];
|
||||
case 'noshow':
|
||||
return [
|
||||
{ icon: Calendar, label: 'Event Created', type: 'trigger' },
|
||||
{ icon: Clock, label: 'Wait 2 Hours Before', type: 'action' },
|
||||
{ icon: MessageSquare, label: 'Send SMS', type: 'action' },
|
||||
];
|
||||
case 'report':
|
||||
return [
|
||||
{ icon: Clock, label: 'Daily at 6 PM', type: 'trigger' },
|
||||
{ icon: FileText, label: "Get Tomorrow's Schedule", type: 'action' },
|
||||
{ icon: Mail, label: 'Send Summary', type: 'action' },
|
||||
];
|
||||
default:
|
||||
return [
|
||||
{ icon: Calendar, label: 'Event Created', type: 'trigger' },
|
||||
{ icon: Clock, label: 'Wait', type: 'action' },
|
||||
{ icon: Mail, label: 'Send Notification', type: 'action' },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const WorkflowVisual: React.FC<WorkflowVisualProps> = ({
|
||||
variant = 'noshow',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const blocks = getWorkflowConfig(variant);
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-xl overflow-hidden">
|
||||
{/* AI Copilot Input */}
|
||||
<div className="p-4 bg-gradient-to-r from-purple-50 to-brand-50 dark:from-purple-900/20 dark:to-brand-900/20 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-600 px-4 py-3 shadow-sm">
|
||||
<Sparkles className="w-5 h-5 text-purple-500" />
|
||||
<span className="text-gray-400 dark:text-gray-500 text-sm flex-1">
|
||||
{t('marketing.plugins.aiCopilot.placeholder')}
|
||||
</span>
|
||||
<motion.div
|
||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
className="w-2 h-5 bg-purple-500 rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 ml-1">
|
||||
{t('marketing.plugins.aiCopilot.examples')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Workflow Visualization */}
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
{blocks.map((block, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{/* Block */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.15 }}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg border ${
|
||||
block.type === 'trigger'
|
||||
? 'bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/30 dark:to-purple-900/30 border-brand-200 dark:border-brand-800'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`p-2 rounded-lg ${
|
||||
block.type === 'trigger'
|
||||
? 'bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<block.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span
|
||||
className={`text-xs font-medium uppercase tracking-wide ${
|
||||
block.type === 'trigger'
|
||||
? 'text-brand-600 dark:text-brand-400'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{block.type === 'trigger' ? 'When' : 'Then'}
|
||||
</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{block.label}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
</motion.div>
|
||||
|
||||
{/* Connector */}
|
||||
{index < blocks.length - 1 && (
|
||||
<div className="flex items-center justify-center h-4">
|
||||
<div className="relative w-0.5 h-full bg-gray-200 dark:bg-gray-700">
|
||||
<motion.div
|
||||
className="absolute w-2 h-2 bg-brand-500 rounded-full left-1/2 -translate-x-1/2"
|
||||
animate={{ y: [0, 12, 0] }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
delay: index * 0.3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Integration badges */}
|
||||
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||
{t('marketing.plugins.integrations.description')}
|
||||
</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{['Gmail', 'Slack', 'Sheets', 'Twilio'].map((app) => (
|
||||
<span
|
||||
key={app}
|
||||
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-md"
|
||||
>
|
||||
{app}
|
||||
</span>
|
||||
))}
|
||||
<span className="px-2 py-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
+1000 more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowVisual;
|
||||
Reference in New Issue
Block a user