Spaces:
Sleeping
Sleeping
| "use client" | |
| import { useState, useEffect } from 'react' | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' | |
| import { Badge } from '@/components/ui/badge' | |
| import { Button } from '@/components/ui/button' | |
| import { | |
| Activity, | |
| Clock, | |
| MapPin, | |
| Building2, | |
| FileText, | |
| Zap, | |
| AlertCircle, | |
| CheckCircle, | |
| Construction, | |
| DollarSign | |
| } from 'lucide-react' | |
| interface DevelopmentUpdate { | |
| id: string | |
| type: 'permit' | 'rezoning' | 'construction' | 'completion' | 'sale' | 'market' | |
| title: string | |
| description: string | |
| location?: string | |
| timestamp: Date | |
| status: 'new' | 'updated' | 'completed' | |
| priority: 'high' | 'medium' | 'low' | |
| value?: number | |
| } | |
| interface DevelopmentFeedProps { | |
| className?: string | |
| } | |
| export default function DevelopmentFeed({ className }: DevelopmentFeedProps) { | |
| const [updates, setUpdates] = useState<DevelopmentUpdate[]>([]) | |
| const [isLive, setIsLive] = useState(true) | |
| // Generate realistic development updates | |
| const generateUpdates = () => { | |
| const sampleUpdates: DevelopmentUpdate[] = [ | |
| { | |
| id: '1', | |
| type: 'permit', | |
| title: 'New Building Permit Issued', | |
| description: '45-story mixed-use tower approved for downtown Vancouver', | |
| location: '1200 W Georgia St, Vancouver', | |
| timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago | |
| status: 'new', | |
| priority: 'high', | |
| value: 85000000 | |
| }, | |
| { | |
| id: '2', | |
| type: 'rezoning', | |
| title: 'Rezoning Application Submitted', | |
| description: 'Request to change from residential to mixed-use development', | |
| location: 'Main St & 15th Ave, Vancouver', | |
| timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4 hours ago | |
| status: 'new', | |
| priority: 'medium' | |
| }, | |
| { | |
| id: '3', | |
| type: 'construction', | |
| title: 'Construction Started', | |
| description: 'Ground breaking ceremony for 28-story residential tower', | |
| location: '789 Cambie St, Vancouver', | |
| timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000), // 6 hours ago | |
| status: 'updated', | |
| priority: 'high', | |
| value: 120000000 | |
| }, | |
| { | |
| id: '4', | |
| type: 'completion', | |
| title: 'Project Completion', | |
| description: 'Luxury condo development ready for occupancy', | |
| location: '456 Beach Ave, Vancouver', | |
| timestamp: new Date(Date.now() - 8 * 60 * 60 * 1000), // 8 hours ago | |
| status: 'completed', | |
| priority: 'medium', | |
| value: 95000000 | |
| }, | |
| { | |
| id: '5', | |
| type: 'market', | |
| title: 'Market Alert', | |
| description: 'Rental rates increase 3.2% in Yaletown district', | |
| location: 'Yaletown, Vancouver', | |
| timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000), // 12 hours ago | |
| status: 'new', | |
| priority: 'medium' | |
| }, | |
| { | |
| id: '6', | |
| type: 'sale', | |
| title: 'Major Property Sale', | |
| description: 'Development site sold to national developer', | |
| location: '123 Granville St, Vancouver', | |
| timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // 1 day ago | |
| status: 'completed', | |
| priority: 'high', | |
| value: 45000000 | |
| } | |
| ] | |
| setUpdates(sampleUpdates) | |
| } | |
| useEffect(() => { | |
| generateUpdates() | |
| // Simulate real-time updates | |
| if (isLive) { | |
| const interval = setInterval(() => { | |
| // Occasionally add a new update | |
| if (Math.random() > 0.7) { | |
| const newUpdate: DevelopmentUpdate = { | |
| id: Date.now().toString(), | |
| type: ['permit', 'rezoning', 'construction', 'market'][Math.floor(Math.random() * 4)] as any, | |
| title: 'Live Update: New Development Activity', | |
| description: 'Real-time market activity detected', | |
| location: 'Vancouver, BC', | |
| timestamp: new Date(), | |
| status: 'new', | |
| priority: 'medium' | |
| } | |
| setUpdates(prev => [newUpdate, ...prev.slice(0, 9)]) // Keep top 10 | |
| } | |
| }, 30000) // Every 30 seconds | |
| return () => clearInterval(interval) | |
| } | |
| }, [isLive]) | |
| const getUpdateIcon = (type: string) => { | |
| switch (type) { | |
| case 'permit': return FileText | |
| case 'rezoning': return AlertCircle | |
| case 'construction': return Construction | |
| case 'completion': return CheckCircle | |
| case 'sale': return DollarSign | |
| case 'market': return Activity | |
| default: return Activity | |
| } | |
| } | |
| const getUpdateColor = (type: string, priority: string) => { | |
| if (priority === 'high') return 'border-l-red-500 bg-red-50' | |
| if (type === 'completion') return 'border-l-green-500 bg-green-50' | |
| if (type === 'permit' || type === 'construction') return 'border-l-blue-500 bg-blue-50' | |
| return 'border-l-gray-500 bg-gray-50' | |
| } | |
| const getBadgeColor = (status: string) => { | |
| switch (status) { | |
| case 'new': return 'bg-green-100 text-green-800' | |
| case 'updated': return 'bg-blue-100 text-blue-800' | |
| case 'completed': return 'bg-gray-100 text-gray-800' | |
| default: return 'bg-gray-100 text-gray-800' | |
| } | |
| } | |
| const formatCurrency = (value: number) => { | |
| return new Intl.NumberFormat('en-CA', { | |
| style: 'currency', | |
| currency: 'CAD', | |
| minimumFractionDigits: 0, | |
| maximumFractionDigits: 0, | |
| }).format(value) | |
| } | |
| const getTimeAgo = (timestamp: Date) => { | |
| const now = new Date() | |
| const diff = now.getTime() - timestamp.getTime() | |
| const hours = Math.floor(diff / (1000 * 60 * 60)) | |
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)) | |
| if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago` | |
| if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago` | |
| return 'Just now' | |
| } | |
| return ( | |
| <div className={className}> | |
| <Card> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-2"> | |
| <Activity className="h-5 w-5 text-green-600" /> | |
| <CardTitle className="flex items-center space-x-2"> | |
| <span>Live Development Feed</span> | |
| {isLive && <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />} | |
| </CardTitle> | |
| </div> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setIsLive(!isLive)} | |
| > | |
| <Zap className="h-4 w-4 mr-2" /> | |
| {isLive ? 'Live' : 'Paused'} | |
| </Button> | |
| </div> | |
| <CardDescription> | |
| Real-time updates on Vancouver development activity and market changes | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-3"> | |
| {updates.length === 0 ? ( | |
| <div className="text-center py-8 text-gray-500"> | |
| <Activity className="h-8 w-8 mx-auto mb-2 opacity-50" /> | |
| <p>No recent updates. Check back soon for live development activity.</p> | |
| </div> | |
| ) : ( | |
| updates.map((update) => { | |
| const Icon = getUpdateIcon(update.type) | |
| return ( | |
| <div | |
| key={update.id} | |
| className={`border-l-4 rounded-lg p-4 ${getUpdateColor(update.type, update.priority)}`} | |
| > | |
| <div className="flex items-start justify-between mb-2"> | |
| <div className="flex items-center space-x-2"> | |
| <Icon className="h-4 w-4 text-gray-600" /> | |
| <h3 className="font-semibold text-gray-900">{update.title}</h3> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Badge className={`text-xs ${getBadgeColor(update.status)}`}> | |
| {update.status} | |
| </Badge> | |
| {update.priority === 'high' && ( | |
| <Badge variant="destructive" className="text-xs"> | |
| High Priority | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| <p className="text-sm text-gray-700 mb-2">{update.description}</p> | |
| <div className="flex items-center justify-between text-xs text-gray-500"> | |
| <div className="flex items-center space-x-4"> | |
| {update.location && ( | |
| <span className="flex items-center"> | |
| <MapPin className="h-3 w-3 mr-1" /> | |
| {update.location} | |
| </span> | |
| )} | |
| {update.value && ( | |
| <span className="flex items-center font-medium text-green-600"> | |
| <DollarSign className="h-3 w-3 mr-1" /> | |
| {formatCurrency(update.value)} | |
| </span> | |
| )} | |
| </div> | |
| <span className="flex items-center"> | |
| <Clock className="h-3 w-3 mr-1" /> | |
| {getTimeAgo(update.timestamp)} | |
| </span> | |
| </div> | |
| </div> | |
| ) | |
| }) | |
| )} | |
| {updates.length > 0 && ( | |
| <div className="mt-4 p-3 bg-gray-50 rounded-lg"> | |
| <p className="text-xs text-gray-600 flex items-center"> | |
| <Activity className="h-3 w-3 mr-1" /> | |
| {isLive ? 'Live feed active' : 'Live feed paused'} • | |
| Last updated: {new Date().toLocaleTimeString()} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ) | |
| } |