Spaces:
Sleeping
Sleeping
| "use client" | |
| import { useState, useEffect, useRef } from 'react' | |
| import { Badge } from '@/components/ui/badge' | |
| import { Button } from '@/components/ui/button' | |
| import { EnhancedCard, EnhancedCardContent, EnhancedCardHeader, EnhancedCardTitle } from '@/components/ui/enhanced-card' | |
| import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' | |
| import { | |
| TrendingUp, | |
| TrendingDown, | |
| BarChart3, | |
| Activity, | |
| Clock, | |
| MapPin, | |
| Building2, | |
| DollarSign, | |
| Zap, | |
| Target, | |
| Users, | |
| Eye, | |
| X, | |
| Maximize2, | |
| Minimize2 | |
| } from 'lucide-react' | |
| export interface PriceData { | |
| timestamp: Date | |
| price: number | |
| change: number | |
| changePercent: number | |
| volume?: number | |
| confidence: 'high' | 'medium' | 'low' | |
| } | |
| export interface MarketMetric { | |
| id: string | |
| name: string | |
| value: number | |
| change: number | |
| changePercent: number | |
| trend: 'up' | 'down' | 'stable' | |
| unit: string | |
| category: 'price' | 'volume' | 'inventory' | 'demand' | |
| } | |
| interface DynamicPricingModalProps { | |
| isOpen: boolean | |
| onClose: () => void | |
| propertyId: string | |
| propertyName: string | |
| propertyAddress: string | |
| currentPrice: number | |
| priceHistory: PriceData[] | |
| marketMetrics: MarketMetric[] | |
| className?: string | |
| } | |
| const formatCurrency = (value: number) => { | |
| return new Intl.NumberFormat('en-CA', { | |
| style: 'currency', | |
| currency: 'CAD', | |
| minimumFractionDigits: 0, | |
| maximumFractionDigits: 0, | |
| }).format(value) | |
| } | |
| const formatNumber = (value: number, unit: string) => { | |
| if (unit === '%') return `${value.toFixed(1)}%` | |
| if (unit === 'units') return value.toLocaleString() | |
| if (unit === 'days') return `${value} days` | |
| return value.toLocaleString() | |
| } | |
| const getConfidenceColor = (confidence: 'high' | 'medium' | 'low') => { | |
| switch (confidence) { | |
| case 'high': return 'text-green-600 bg-green-50' | |
| case 'medium': return 'text-yellow-600 bg-yellow-50' | |
| case 'low': return 'text-red-600 bg-red-50' | |
| } | |
| } | |
| const getTrendIcon = (trend: 'up' | 'down' | 'stable') => { | |
| switch (trend) { | |
| case 'up': return TrendingUp | |
| case 'down': return TrendingDown | |
| case 'stable': return Activity | |
| } | |
| } | |
| const getTrendColor = (trend: 'up' | 'down' | 'stable') => { | |
| switch (trend) { | |
| case 'up': return 'text-green-600' | |
| case 'down': return 'text-red-600' | |
| case 'stable': return 'text-gray-600' | |
| } | |
| } | |
| export default function DynamicPricingModal({ | |
| isOpen, | |
| onClose, | |
| propertyId, | |
| propertyName, | |
| propertyAddress, | |
| currentPrice, | |
| priceHistory, | |
| marketMetrics, | |
| className | |
| }: DynamicPricingModalProps) { | |
| const [isExpanded, setIsExpanded] = useState(false) | |
| const [selectedTimeframe, setSelectedTimeframe] = useState<'1D' | '1W' | '1M' | '3M' | '1Y'>('1D') | |
| const [livePrice, setLivePrice] = useState(currentPrice) | |
| const [priceChange, setPriceChange] = useState(0) | |
| const [isAnimating, setIsAnimating] = useState(false) | |
| const chartRef = useRef<HTMLDivElement>(null) | |
| // Simulate live price updates | |
| useEffect(() => { | |
| if (!isOpen) return | |
| const interval = setInterval(() => { | |
| const variation = (Math.random() - 0.5) * 0.02 // ±1% variation | |
| const newPrice = currentPrice * (1 + variation) | |
| const change = newPrice - livePrice | |
| setIsAnimating(true) | |
| setLivePrice(newPrice) | |
| setPriceChange(change) | |
| setTimeout(() => setIsAnimating(false), 500) | |
| }, 3000) | |
| return () => clearInterval(interval) | |
| }, [isOpen, currentPrice, livePrice]) | |
| const latestData = priceHistory[priceHistory.length - 1] | |
| const changePercent = ((livePrice - currentPrice) / currentPrice) * 100 | |
| return ( | |
| <Dialog open={isOpen} onOpenChange={onClose}> | |
| <DialogContent | |
| className={`${isExpanded ? 'max-w-6xl' : 'max-w-4xl'} transition-all duration-300 ${className}`} | |
| > | |
| <DialogHeader className="border-b border-gray-200 pb-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="p-2 bg-blue-100 rounded-full"> | |
| <Building2 className="h-5 w-5 text-blue-600" /> | |
| </div> | |
| <div> | |
| <DialogTitle className="text-xl font-bold text-gray-900"> | |
| {propertyName} | |
| </DialogTitle> | |
| <p className="text-sm text-gray-600 flex items-center mt-1"> | |
| <MapPin className="h-3 w-3 mr-1" /> | |
| {propertyAddress} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setIsExpanded(!isExpanded)} | |
| > | |
| {isExpanded ? ( | |
| <Minimize2 className="h-4 w-4" /> | |
| ) : ( | |
| <Maximize2 className="h-4 w-4" /> | |
| )} | |
| </Button> | |
| <Button variant="outline" size="sm" onClick={onClose}> | |
| <X className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </DialogHeader> | |
| <div className="grid grid-cols-12 gap-6 py-6"> | |
| {/* Live Price Display */} | |
| <div className="col-span-12 lg:col-span-4"> | |
| <EnhancedCard variant="floating" className="text-center"> | |
| <EnhancedCardHeader> | |
| <div className="flex items-center justify-center space-x-2 mb-2"> | |
| <Zap className="h-4 w-4 text-yellow-500 animate-pulse" /> | |
| <span className="text-sm text-gray-600 font-medium">LIVE PRICE</span> | |
| <Badge variant="destructive" className="animate-pulse"> | |
| LIVE | |
| </Badge> | |
| </div> | |
| </EnhancedCardHeader> | |
| <EnhancedCardContent> | |
| <div className={`text-4xl font-bold mb-2 transition-all duration-500 ${ | |
| isAnimating ? 'scale-105 text-blue-600' : 'text-gray-900' | |
| }`}> | |
| {formatCurrency(livePrice)} | |
| </div> | |
| <div className={`flex items-center justify-center space-x-2 text-lg ${ | |
| changePercent >= 0 ? 'text-green-600' : 'text-red-600' | |
| }`}> | |
| {changePercent >= 0 ? ( | |
| <TrendingUp className="h-4 w-4" /> | |
| ) : ( | |
| <TrendingDown className="h-4 w-4" /> | |
| )} | |
| <span>{formatCurrency(Math.abs(priceChange))}</span> | |
| <span>({Math.abs(changePercent).toFixed(2)}%)</span> | |
| </div> | |
| <div className="mt-4 grid grid-cols-2 gap-4 text-sm"> | |
| <div className="text-center p-3 bg-blue-50 rounded-lg"> | |
| <div className="font-semibold text-blue-900">24h High</div> | |
| <div className="text-blue-600"> | |
| {formatCurrency(currentPrice * 1.05)} | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-purple-50 rounded-lg"> | |
| <div className="font-semibold text-purple-900">24h Low</div> | |
| <div className="text-purple-600"> | |
| {formatCurrency(currentPrice * 0.95)} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="mt-4"> | |
| <div className={`text-xs px-2 py-1 rounded-full inline-flex items-center ${ | |
| getConfidenceColor(latestData?.confidence || 'medium') | |
| }`}> | |
| <Target className="h-3 w-3 mr-1" /> | |
| {latestData?.confidence.toUpperCase()} CONFIDENCE | |
| </div> | |
| </div> | |
| </EnhancedCardContent> | |
| </EnhancedCard> | |
| </div> | |
| {/* Price Chart Visualization */} | |
| <div className="col-span-12 lg:col-span-8"> | |
| <EnhancedCard variant="elevated"> | |
| <EnhancedCardHeader> | |
| <div className="flex items-center justify-between"> | |
| <EnhancedCardTitle className="flex items-center space-x-2"> | |
| <BarChart3 className="h-5 w-5" /> | |
| <span>Price Movement</span> | |
| </EnhancedCardTitle> | |
| <div className="flex space-x-1"> | |
| {(['1D', '1W', '1M', '3M', '1Y'] as const).map((timeframe) => ( | |
| <Button | |
| key={timeframe} | |
| variant={selectedTimeframe === timeframe ? 'default' : 'outline'} | |
| size="xs" | |
| onClick={() => setSelectedTimeframe(timeframe)} | |
| className="px-2 py-1 text-xs" | |
| > | |
| {timeframe} | |
| </Button> | |
| ))} | |
| </div> | |
| </div> | |
| </EnhancedCardHeader> | |
| <EnhancedCardContent> | |
| {/* Simplified chart visualization */} | |
| <div ref={chartRef} className="h-48 bg-gray-50 rounded-lg p-4 flex items-end space-x-1 overflow-hidden"> | |
| {priceHistory.slice(-20).map((data, index) => { | |
| const height = ((data.price - Math.min(...priceHistory.map(d => d.price))) / | |
| (Math.max(...priceHistory.map(d => d.price)) - Math.min(...priceHistory.map(d => d.price)))) * 160 + 20 | |
| return ( | |
| <div | |
| key={index} | |
| className={`flex-1 rounded-t transition-all duration-300 ${ | |
| data.change >= 0 ? 'bg-green-400' : 'bg-red-400' | |
| }`} | |
| style={{ height: `${height}px` }} | |
| title={`${formatCurrency(data.price)} (${data.changePercent > 0 ? '+' : ''}${data.changePercent.toFixed(2)}%)`} | |
| /> | |
| ) | |
| })} | |
| {/* Live price indicator */} | |
| <div | |
| className={`w-2 rounded-t bg-blue-500 animate-pulse transition-all duration-500 ${ | |
| isAnimating ? 'shadow-lg shadow-blue-300' : '' | |
| }`} | |
| style={{ height: '140px' }} | |
| /> | |
| </div> | |
| <div className="mt-4 flex items-center justify-between text-xs text-gray-600"> | |
| <span>Past 20 intervals</span> | |
| <div className="flex items-center space-x-4"> | |
| <div className="flex items-center space-x-1"> | |
| <div className="w-3 h-3 bg-green-400 rounded"></div> | |
| <span>Price Up</span> | |
| </div> | |
| <div className="flex items-center space-x-1"> | |
| <div className="w-3 h-3 bg-red-400 rounded"></div> | |
| <span>Price Down</span> | |
| </div> | |
| <div className="flex items-center space-x-1"> | |
| <div className="w-3 h-3 bg-blue-500 rounded animate-pulse"></div> | |
| <span>Live</span> | |
| </div> | |
| </div> | |
| </div> | |
| </EnhancedCardContent> | |
| </EnhancedCard> | |
| </div> | |
| {/* Market Metrics */} | |
| <div className="col-span-12"> | |
| <EnhancedCard variant="elevated"> | |
| <EnhancedCardHeader> | |
| <EnhancedCardTitle className="flex items-center space-x-2"> | |
| <Activity className="h-5 w-5" /> | |
| <span>Live Market Metrics</span> | |
| </EnhancedCardTitle> | |
| </EnhancedCardHeader> | |
| <EnhancedCardContent> | |
| <div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> | |
| {marketMetrics.map((metric) => { | |
| const TrendIcon = getTrendIcon(metric.trend) | |
| const trendColor = getTrendColor(metric.trend) | |
| return ( | |
| <div | |
| key={metric.id} | |
| className="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors" | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <h4 className="text-sm font-medium text-gray-700">{metric.name}</h4> | |
| <TrendIcon className={`h-3 w-3 ${trendColor}`} /> | |
| </div> | |
| <div className="text-lg font-bold text-gray-900 mb-1"> | |
| {formatNumber(metric.value, metric.unit)} | |
| </div> | |
| <div className={`text-xs flex items-center ${ | |
| metric.changePercent >= 0 ? 'text-green-600' : 'text-red-600' | |
| }`}> | |
| <span> | |
| {metric.changePercent >= 0 ? '+' : ''} | |
| {metric.changePercent.toFixed(1)}% | |
| </span> | |
| <span className="ml-1 text-gray-500">vs yesterday</span> | |
| </div> | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </EnhancedCardContent> | |
| </EnhancedCard> | |
| </div> | |
| {/* Quick Actions */} | |
| <div className="col-span-12 flex items-center justify-between pt-4 border-t border-gray-200"> | |
| <div className="flex items-center space-x-2 text-sm text-gray-600"> | |
| <Clock className="h-4 w-4" /> | |
| <span>Last updated: {new Date().toLocaleTimeString()}</span> | |
| <Badge variant="secondary" className="ml-2"> | |
| Auto-refresh: 3s | |
| </Badge> | |
| </div> | |
| <div className="flex space-x-2"> | |
| <Button variant="outline" size="sm"> | |
| <Eye className="h-3 w-3 mr-1" /> | |
| Watch Property | |
| </Button> | |
| <Button size="sm"> | |
| <Users className="h-3 w-3 mr-1" /> | |
| Contact Agent | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| ) | |
| } |