simpse-demo-fixed / src /components /DevelopmentFeed.tsx
Simeon G
Deploy DataBeast to Hugging Face Spaces
aa72857
Raw
History Blame Contribute Delete
10.1 kB
"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>
)
}