Files
AIHostingTycoon/apps/web/src/pages/market/ConsumerTiersPanel.tsx
T
josh 09a5cb69a7
CI / build-and-push (push) Successful in 42s
Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
Replaces the simplified single-subscriber market with a full competitive simulation:
shared TAM with softmax market shares across 4 segments, multi-tier consumer
subscriptions (Free/Plus/Pro/Team) and API tiers (Free/PAYG/Scale/Enterprise),
enterprise sales pipeline (Lead→Qualification→POC→Negotiation→Active→Renewal)
with SLA tracking, developer ecosystem flywheel, technology obsolescence pressure,
seasonal demand cycles, and two new product lines (Code Assistant, AI Agents Platform).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 08:30:24 -04:00

110 lines
4.9 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react';
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import type { ConsumerTierId } from '@ai-tycoon/shared';
import { Users, Check } from 'lucide-react';
const TIER_ORDER: ConsumerTierId[] = ['free', 'plus', 'pro', 'team'];
const TIER_COLORS: Record<ConsumerTierId, string> = {
free: 'border-surface-500',
plus: 'border-blue-500',
pro: 'border-purple-500',
team: 'border-orange-500',
};
function useAppliedFeedback() {
const [show, setShow] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const trigger = useCallback(() => {
setShow(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setShow(false), 1200);
}, []);
useEffect(() => () => clearTimeout(timerRef.current), []);
return { show, trigger };
}
export function ConsumerTiersPanel() {
const consumerTiers = useGameStore((s) => s.market.consumerTiers);
const setConsumerTierPrice = useGameStore((s) => s.setConsumerTierPrice);
const toggleConsumerTier = useGameStore((s) => s.toggleConsumerTier);
const feedback = useAppliedFeedback();
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Users size={16} className="text-orange-400" />
<span className="text-sm font-semibold">Consumer Subscriptions</span>
</div>
<div className="flex items-center gap-4 text-xs text-surface-400">
<span>Total: <span className="font-mono text-surface-200">{formatNumber(consumerTiers.totalUsers)}</span></span>
<span>Satisfaction: <span className={`font-mono ${consumerTiers.satisfaction > 0.7 ? 'text-success' : consumerTiers.satisfaction > 0.4 ? 'text-warning' : 'text-danger'}`}>{formatPercent(consumerTiers.satisfaction)}</span></span>
</div>
</div>
<div className="grid grid-cols-4 gap-3">
{TIER_ORDER.map(tierId => {
const tier = consumerTiers.tiers[tierId];
const revenue = tierId === 'free' ? 0 : tier.userCount * tier.config.price / 86400;
return (
<div key={tierId} className={`bg-surface-900 border-t-2 ${TIER_COLORS[tierId]} border border-surface-700 rounded-xl p-4 space-y-3`}>
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm">{tier.config.name}</h4>
{tierId !== 'free' && (
<button
onClick={() => { toggleConsumerTier(tierId); feedback.trigger(); }}
className={`text-[10px] px-2 py-0.5 rounded-full ${tier.config.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
>
{tier.config.isActive ? 'Active' : 'Inactive'}
</button>
)}
{tierId === 'free' && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-surface-700 text-surface-400">Always On</span>
)}
</div>
<div className="text-2xl font-bold font-mono">{formatNumber(tier.userCount)}</div>
<div className="text-xs text-surface-400">users</div>
{tierId !== 'free' && (
<div>
<label className="block text-[10px] text-surface-400 mb-1">
Price ($/mo)
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
</label>
<input
type="number"
value={tier.config.price}
onChange={(e) => { setConsumerTierPrice(tierId, Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={1}
step={5}
/>
</div>
)}
<div className="text-xs space-y-1 text-surface-400">
<div className="flex justify-between">
<span>Tokens/mo</span>
<span className="font-mono">{formatNumber(tier.config.tokenAllowance)}</span>
</div>
<div className="flex justify-between">
<span>Churn</span>
<span className="font-mono">{formatPercent(tier.churnRate)}/t</span>
</div>
{tierId !== 'free' && (
<div className="flex justify-between">
<span>Revenue/s</span>
<span className="font-mono text-success">{formatMoney(revenue)}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}