Загрузка...

UI компонент: калькулятор цен ClickUp. Интерактивный выбор приложений, ввод пользователей, расчет стоимости. Помогает сравнить затраты.
# ClickUp Pricing Calculator
You are given a task to integrate an existing React component in the codebase
~~~/README.md
# ClickUp Cost Calculator Component
A high-fidelity cost savings calculator that compares spending on multiple SaaS tools versus consolidating with ClickUp.
## Features
- **Interactive App Grid**: Select from 20+ common SaaS tools (Slack, Jira, Asana, etc.)
- **Dynamic Cost Calculation**: Real-time updates based on user count and selected apps
- **Visual Feedback**: Gradient accents, hover effects, and responsive layout
- **Custom Icons**: High-quality SVG icons for major brands
## Props
| Prop | Type | Description |
|------|------|-------------|
| `className` | `string` | Optional CSS classes to override styles |
## Usage
```tsx
import { ClickUpCostCalculator } from '@/sd-components/7442a078-e1c9-437e-87ce-a7715cbd1439';
function PricingPage() {
return (
<div className="py-20">
<ClickUpCostCalculator />
</div>
);
}
```
~~~
~~~/src/App.tsx
import React from 'react';
import { ClickUpCostCalculator } from './Component';
export default function App() {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-slate-50 p-4 md:p-10">
<ClickUpCostCalculator />
</div>
);
}
~~~
~~~/src/Component.tsx
import React, { useState, useMemo } from 'react';
import {
Check,
Plus,
Minus,
ChevronRight,
MessageSquare,
HardDrive,
Cloud,
Video,
Trello,
FileText,
Layout,
Database,
Briefcase,
Slack,
Table
} from 'lucide-react';
import { cn } from '@/lib/utils'; // Assuming generic utility, but I will inline clsx logic if needed since I don't know if lib/utils exists. actually I'll just use template literals or inline logic.
// --- Icons & Data ---
const CustomIcons = {
Slack: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M5.042 15.123a2.52 2.52 0 0 1-2.52 2.52 2.52 2.52 0 0 1-2.52-2.52 2.52 2.52 0 0 1 2.52-2.52h2.52v2.52Zm.84-2.52a2.52 2.52 0 0 1 2.52-2.52 2.52 2.52 0 0 1 2.52 2.52v2.52h-2.52a2.52 2.52 0 0 1-2.52-2.52Zm0-6.72a2.52 2.52 0 0 1-2.52-2.52 2.52 2.52 0 0 1 2.52-2.52 2.52 2.52 0 0 1 2.52 2.52v2.52h-2.52Zm3.36 2.52a2.52 2.52 0 0 1-2.52 2.52 2.52 2.52 0 0 1-2.52-2.52V5.882a2.52 2.52 0 0 1 2.52-2.52 2.52 2.52 0 0 1 2.52 2.52v2.52Zm8.4 0a2.52 2.52 0 0 1 2.52-2.52 2.52 2.52 0 0 1 2.52 2.52 2.52 2.52 0 0 1-2.52 2.52h-2.52V8.402Zm-.84 2.52a2.52 2.52 0 0 1-2.52 2.52 2.52 2.52 0 0 1-2.52-2.52V5.882a2.52 2.52 0 0 1 2.52-2.52 2.52 2.52 0 0 1 2.52 2.52v5.04Zm0 6.72a2.52 2.52 0 0 1 2.52 2.52 2.52 2.52 0 0 1-2.52 2.52 2.52 2.52 0 0 1-2.52-2.52v-2.52h2.52Zm-3.36-2.52a2.52 2.52 0 0 1 2.52-2.52 2.52 2.52 0 0 1 2.52 2.52v2.52a2.52 2.52 0 0 1-2.52 2.52 2.52 2.52 0 0 1-2.52-2.52v-2.52Z" fill="currentColor"/>
</svg>
),
GoogleDrive: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M7.94 4.14h8.12l5.65 9.77H13.6L7.94 4.14zM14.63 15.65L9 5.86 2.29 17.5h11.33l1.01-1.85zM9.54 16.58l-2.84 4.92h11.33l2.84-4.92H9.54z" fill="currentColor"/>
</svg>
),
Salesforce: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M16.5 6c-.8 0-1.5.2-2.1.6C13.8 4.5 11.8 3 9.5 3 5.9 3 3 5.9 3 9.5c0 .6.1 1.2.2 1.8C1.9 12.1 1 13.5 1 15c0 2.2 1.8 4 4 4h11.5c3 0 5.5-2.5 5.5-5.5S19.5 8 16.5 6z" fill="currentColor"/>
</svg>
),
Hubspot: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
<path d="M12 15v4" stroke="currentColor" strokeWidth="2"/>
<path d="M15 12h4" stroke="currentColor" strokeWidth="2"/>
<path d="M12 9V5" stroke="currentColor" strokeWidth="2"/>
<path d="M9 12H5" stroke="currentColor" strokeWidth="2"/>
</svg>
),
OpenAI: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M20.9 10.2c-.3-1.6-1.5-2.9-3-3.4.4-1.6-.1-3.3-1.2-4.5-1.3-1.3-3.2-1.7-4.8-1-1.2-1.4-3.2-1.9-4.9-1.1-1.8.8-2.9 2.6-2.7 4.5C2.6 5.1 1.4 6.4 1 8c-.4 1.6.1 3.3 1.2 4.5 1.3 1.3 3.2 1.7 4.8 1 1.2 1.4 3.2 1.9 4.9 1.1 1.8-.8 2.9-2.6 2.7-4.5 1.7-.4 2.9-1.7 3.3-3.3l3-1.4zM12 15c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z" stroke="currentColor" strokeWidth="1.5"/>
</svg>
),
Jira: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M11.5 11l-4.2-4.2c-1.2-1.2-3.1-1.2-4.2 0s-1.2 3.1 0 4.2l4.2 4.2 4.2-4.2z" fill="currentColor" fillOpacity="0.5"/>
<path d="M11.5 19.5L7.3 15.3l4.2-4.2 4.2 4.2c1.2 1.2 1.2 3.1 0 4.2-1.1 1.2-3.1 1.2-4.2 0z" fill="currentColor"/>
<path d="M19.9 11l-4.2-4.2-4.2 4.2 4.2 4.2c1.2 1.2 3.1 1.2 4.2 0 1.2-1.1 1.2-3 0-4.2z" fill="currentColor"/>
</svg>
),
Loom: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="12" cy="12" r="10" fill="currentColor"/>
<path d="M12 7v10M7 12h10" stroke="#fff" strokeWidth="2"/>
</svg>
),
Notion: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M4.5 5.5v13h15v-13h-15zm2 2h2v9h-2v-9zm4 0h6v2h-6v-2zm0 4h6v2h-6v-2zm0 4h4v2h-4v-2z" fill="currentColor"/>
</svg>
),
Monday: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M3 3h18v18H3V3zm4 14h2.5v-7h-2.5v7zm3.75 0h2.5V8h-2.5v9zm3.75 0h2.5v-5h-2.5v5z" fill="currentColor"/>
</svg>
),
Trello: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<rect x="3" y="3" width="18" height="18" rx="2" fill="currentColor" fillOpacity="0.2"/>
<rect x="6" y="6" width="5" height="10" rx="1" fill="currentColor"/>
<rect x="13" y="6" width="5" height="7" rx="1" fill="currentColor"/>
</svg>
),
Asana: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="12" cy="12" r="4" fill="currentColor"/>
<circle cx="18" cy="6" r="2.5" fill="currentColor" fillOpacity="0.6"/>
<circle cx="6" cy="18" r="2.5" fill="currentColor" fillOpacity="0.6"/>
</svg>
),
Airtable: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M12 2L2 8l10 6 10-6-10-6zm0 13.5L3.5 10v6.5l8.5 5.5 8.5-5.5V10L12 15.5z" fill="currentColor"/>
</svg>
),
Linear: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.8l7 3.5-7 3.5-7-3.5 7-3.5zm-8 5.7l7 3.5v7l-7-3.5v-7zm9 10.5v-7l7-3.5v7l-7 3.5z" fill="currentColor"/>
</svg>
),
ClickUp: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M3 13.5l1.5-3 7.5-4.5 7.5 4.5 1.5 3-9-3-9 3z" fill="currentColor"/>
<path d="M12 22l-9-5 1.5-3 7.5 3 7.5-3 1.5 3-9 5z" fill="currentColor"/>
</svg>
),
Generic: (props: any) => (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<rect x="2" y="2" width="20" height="20" rx="4" fill="currentColor" fillOpacity="0.2"/>
<path d="M12 7v10M7 12h10" stroke="currentColor" strokeWidth="2"/>
</svg>
)
};
// Define app data
const APPS = [
{ id: 'slack', name: 'Slack', cost: 9, icon: CustomIcons.Slack, color: '#4A154B' },
{ id: 'teams', name: 'Teams', cost: 6, icon: MessageSquare, color: '#6264A7' },
{ id: 'drive', name: 'Google Drive', cost: 13, icon: CustomIcons.GoogleDrive, color: '#34A853' },
{ id: 'salesforce', name: 'Salesforce', cost: 25, icon: CustomIcons.Salesforce, color: '#00A1E0' },
{ id: 'hubspot', name: 'HubSpot', cost: 18, icon: CustomIcons.Hubspot, color: '#FF7A59' },
{ id: 'openai', name: 'ChatGPT', cost: 20, icon: CustomIcons.OpenAI, color: '#10A37F' },
{ id: 'jira', name: 'Jira', cost: 8, icon: CustomIcons.Jira, color: '#0052CC' },
{ id: 'loom', name: 'Loom', cost: 12, icon: CustomIcons.Loom, color: '#625DF5' },
{ id: 'notion', name: 'Notion', cost: 10, icon: CustomIcons.Notion, color: '#000000' },
{ id: 'monday', name: 'Monday', cost: 16, icon: CustomIcons.Monday, color: '#F65F5C' },
{ id: 'trello', name: 'Trello', cost: 5, icon: CustomIcons.Trello, color: '#0079BF' },
{ id: 'asana', name: 'Asana', cost: 11, icon: CustomIcons.Asana, color: '#F06A6A' },
{ id: 'smartsheet', name: 'Smartsheet', cost: 7, icon: FileText, color: '#1F4F9A' },
{ id: 'airtable', name: 'Airtable', cost: 20, icon: CustomIcons.Airtable, color: '#FCB400' },
{ id: 'linear', name: 'Linear', cost: 8, icon: CustomIcons.Linear, color: '#5E6AD2' },
{ id: 'confluence', name: 'Confluence', cost: 5, icon: Layout, color: '#172B4D' },
{ id: 'clickup', name: 'ClickUp', cost: 0, icon: CustomIcons.ClickUp, color: '#7B2CBF' } // Cost is handled separately
];
interface ClickUpCostCalculatorProps {
className?: string;
}
export function ClickUpCostCalculator({ className }: ClickUpCostCalculatorProps) {
const [selectedApps, setSelectedApps] = useState<string[]>(['slack', 'drive', 'salesforce', 'loom']);
const [userCount, setUserCount] = useState<number>(500);
const toggleApp = (id: string) => {
setSelectedApps(prev =>
prev.includes(id) ? prev.filter(appId => appId !== id) : [...prev, id]
);
};
const handleUserCountChange = (delta: number) => {
setUserCount(prev => Math.max(1, prev + delta));
};
const handleUserCountInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = parseInt(e.target.value) || 0;
setUserCount(val);
};
const selectedAppsData = useMemo(() =>
APPS.filter(app => selectedApps.includes(app.id)),
[selectedApps]);
const totalAppCost = useMemo(() =>
selectedAppsData.reduce((acc, app) => acc + app.cost, 0) * userCount * 12,
[selectedAppsData, userCount]);
const clickUpCostPerUser = 12; // Monthly
const clickUpTotalCost = clickUpCostPerUser * userCount * 12;
const savings = totalAppCost - clickUpTotalCost;
return (
<section className={cn("w-full max-w-5xl mx-auto p-4 font-sans", className)}>
<div className="text-center mb-10">
<h2 className="text-4xl md:text-5xl font-bold tracking-tight mb-3 text-slate-900">
Cut Costs with ClickUp
</h2>
<p className="text-slate-600 text-lg">
Cost savings are based on average monthly price per user for each app.
</p>
</div>
<div className="relative rounded-3xl p-2 md:p-3 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-600 shadow-xl">
<div className="flex flex-col md:flex-row gap-2 md:gap-3 bg-transparent">
{/* Left Panel: App Selection */}
<div className="flex-1 bg-white rounded-2xl p-6 md:p-8 flex flex-col min-h-[500px]">
<h3 className="text-xl font-medium text-slate-900 mb-6">Which apps do you use?</h3>
<div className="grid grid-cols-4 sm:grid-cols-5 gap-3 md:gap-4 mb-auto">
{APPS.slice(0, 20).filter(a => a.id !== 'clickup').map((app) => {
const isSelected = selectedApps.includes(app.id);
const Icon = app.icon;
return (
<button
key={app.id}
onClick={() => toggleApp(app.id)}
className={cn(
"group relative aspect-square flex items-center justify-center rounded-2xl border-2 transition-all duration-200 ease-out p-3",
isSelected
? "border-purple-500 bg-purple-50 shadow-sm"
: "border-slate-100 hover:border-slate-300 bg-white hover:bg-slate-50"
)}
aria-label={`Select ${app.name}`}
>
{/* Checkmark badge */}
{isSelected && (
<div className="absolute -top-2 -right-2 w-5 h-5 bg-purple-600 rounded-full flex items-center justify-center text-white ring-2 ring-white z-10 shadow-sm">
<Check size={12} strokeWidth={3} />
</div>
)}
{/* Icon */}
<div
className={cn(
"w-full h-full flex items-center justify-center transition-transform duration-200",
isSelected ? "scale-110" : "group-hover:scale-110"
)}
style={{ color: isSelected ? app.color : '#64748b' }} // Color when selected, gray when not
>
<Icon className="w-8 h-8 md:w-10 md:h-10" strokeWidth={1.5} />
</div>
</button>
);
})}
</div>
<div className="mt-8 pt-6 border-t border-slate-100">
<label className="block text-slate-900 font-medium mb-3">
How many people work at your company?
</label>
<div className="flex items-center gap-3">
<button
onClick={() => handleUserCountChange(-10)}
className="w-12 h-12 flex items-center justify-center rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors"
>
<Minus size={20} />
</button>
<div className="relative">
<input
type="number"
value={userCount}
onChange={handleUserCountInput}
className="w-32 h-12 text-center text-xl font-bold bg-white border-2 border-slate-200 rounded-xl focus:border-purple-500 focus:outline-none transition-all"
/>
</div>
<button
onClick={() => handleUserCountChange(10)}
className="w-12 h-12 flex items-center justify-center rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors"
>
<Plus size={20} />
</button>
</div>
</div>
</div>
{/* Divider Circle */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20 hidden md:flex">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center shadow-lg ring-4 ring-white">
<div className="text-white">
<div className="flex -space-x-1">
<ChevronRight size={20} strokeWidth={3} />
<ChevronRight size={20} strokeWidth={3} />
</div>
</div>
</div>
</div>
{/* Right Panel: Savings Calculation */}
<div className="flex-1 bg-white rounded-2xl p-6 md:p-8 flex flex-col justify-between min-h-[500px]">
<div>
<div className="flex justify-between items-baseline mb-2">
<h3 className="text-xl font-medium text-slate-900">Apps to replace</h3>
<span className="text-sm text-slate-400 font-medium">for <span className="text-slate-900 font-bold">{userCount}</span> users</span>
</div>
<div className="space-y-4 mb-8">
{selectedAppsData.length === 0 ? (
<div className="text-slate-400 italic py-4">Select apps to see potential savings...</div>
) : (
<div className="space-y-3">
{selectedAppsData.map(app => (
<div key={app.id} className="flex items-center justify-between text-slate-600 group hover:bg-slate-50 p-2 -mx-2 rounded-lg transition-colors">
<div className="flex items-center gap-3">
{/* Small Icon */}
<div style={{ color: app.color }}><app.icon className="w-5 h-5" /></div>
<span className="font-medium">{app.name}</span>
</div>
<div className="flex items-center gap-1">
<span className="font-semibold text-slate-900">${app.cost}</span>
<span className="text-sm text-slate-400">/ user</span>
</div>
</div>
))}
</div>
)}
<div className="border-b border-slate-200 my-4" />
<div className="flex items-center justify-between font-bold text-lg text-slate-900">
<span>TOTAL</span>
<span>${totalAppCost.toLocaleString()} / year</span>
</div>
<div className="border-b border-dotted border-slate-300 my-4" />
<div className="text-slate-600">
ClickUp for {userCount} users = <span className="font-semibold text-slate-900">${clickUpTotalCost.toLocaleString()} / year</span>
</div>
</div>
</div>
<div className="mt-auto">
<div className="mb-6">
<h4 className="text-4xl md:text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600 mb-2">
Save ${Math.max(0, savings).toLocaleString()}/year
</h4>
<p className="text-slate-500 text-sm leading-relaxed">
ClickUp can save a {userCount} person company ${Math.max(0, savings).toLocaleString()} per year compared to the non-enterprise price of your apps.
</p>
</div>
<button className="w-full py-4 px-6 rounded-xl bg-gradient-to-r from-blue-600 via-indigo-600 to-purple-600 text-white font-bold text-lg shadow-lg shadow-purple-200 hover:shadow-xl hover:shadow-purple-300 hover:scale-[1.02] transition-all duration-200 flex items-center justify-center gap-2 group">
Start saving with ClickUp today
<ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</button>
<div className="mt-4 text-center">
<a href="#" className="text-sm text-purple-600 hover:text-purple-700 font-medium hover:underline">
Get an official quote
</a>
<span className="text-sm text-slate-500"> to share with your team</span>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
export default ClickUpCostCalculator;
~~~
Implementation Guidelines
1. Analyze the component structure, styling, animation implementations
2. Review the component's arguments and state
3. Think through what is the best place to adopt this component/style into the design we are doing
4. Then adopt the component/design to our current system
Help me integrate this into my design