- Add 30m and 2h timeframes to available options - Update Scalping preset: 5m, 15m, 30m (was 5m, 15m, 1h) - Update Day Trading preset: 1h, 2h, 4h (was 1h, 4h, 1d) - Enhance sorting logic for screenshots and analysis results - Ensure consistent timeframe order: 5m → 15m → 30m → 1h → 2h → 4h → 1d - Improve multi-timeframe analysis display with proper sorting - Update filename parsing to handle new timeframes Changes improve trading workflow with more logical timeframe progressions for scalping and day trading strategies.
217 lines
8.2 KiB
TypeScript
217 lines
8.2 KiB
TypeScript
"use client"
|
|
import React, { useEffect } from 'react'
|
|
|
|
interface ScreenshotGalleryProps {
|
|
screenshots: string[]
|
|
symbol: string
|
|
timeframes: string[]
|
|
enlargedImage: string | null
|
|
onImageClick: (src: string) => void
|
|
onClose: () => void
|
|
}
|
|
|
|
export default function ScreenshotGallery({
|
|
screenshots,
|
|
symbol,
|
|
timeframes,
|
|
enlargedImage,
|
|
onImageClick,
|
|
onClose
|
|
}: ScreenshotGalleryProps) {
|
|
// Handle ESC key to close enlarged image
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape' && enlargedImage) {
|
|
onClose()
|
|
}
|
|
}
|
|
|
|
if (enlargedImage) {
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
}
|
|
}, [enlargedImage, onClose])
|
|
|
|
if (screenshots.length === 0) return null
|
|
|
|
// Utility function to convert timeframe to sortable number
|
|
const timeframeToMinutes = (timeframe: string): number => {
|
|
const tf = timeframe.toLowerCase()
|
|
if (tf.includes('5m') || tf === '5') return 5
|
|
if (tf.includes('15m') || tf === '15') return 15
|
|
if (tf.includes('30m') || tf === '30') return 30
|
|
if (tf.includes('1h') || tf === '60') return 60
|
|
if (tf.includes('2h') || tf === '120') return 120
|
|
if (tf.includes('4h') || tf === '240') return 240
|
|
if (tf.includes('1d') || tf === 'D') return 1440
|
|
// Default fallback
|
|
return parseInt(tf) || 999
|
|
}
|
|
|
|
// Extract timeframe from filename
|
|
const extractTimeframeFromFilename = (filename: string) => {
|
|
const match = filename.match(/_(\d+|D)_/)
|
|
if (!match) return 'Unknown'
|
|
const tf = match[1]
|
|
if (tf === 'D') return '1D'
|
|
if (tf === '5') return '5m'
|
|
if (tf === '15') return '15m'
|
|
if (tf === '30') return '30m'
|
|
if (tf === '60') return '1h'
|
|
if (tf === '120') return '2h'
|
|
if (tf === '240') return '4h'
|
|
return `${tf}m`
|
|
}
|
|
|
|
// Create sorted screenshot data with timeframes
|
|
const screenshotData = screenshots.map((screenshot, index) => {
|
|
const screenshotUrl = typeof screenshot === 'string'
|
|
? screenshot
|
|
: (screenshot as any)?.url || String(screenshot)
|
|
const filename = screenshotUrl.split('/').pop() || ''
|
|
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
|
|
|
|
return {
|
|
screenshot,
|
|
screenshotUrl,
|
|
filename,
|
|
timeframe,
|
|
index,
|
|
sortOrder: timeframeToMinutes(timeframe)
|
|
}
|
|
})
|
|
|
|
// Sort by timeframe (smallest to largest)
|
|
const sortedData = screenshotData.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
|
|
// Helper function to format screenshot URL
|
|
const formatScreenshotUrl = (screenshot: string | any) => {
|
|
// Handle both string URLs and screenshot objects
|
|
const screenshotUrl = typeof screenshot === 'string' ? screenshot : screenshot.url || screenshot
|
|
// Extract just the filename from the full path
|
|
const filename = screenshotUrl.split('/').pop() || screenshotUrl
|
|
// Use the new API route with query parameter
|
|
return `/api/image?file=${filename}`
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Gallery Grid */}
|
|
<div className="mt-6 p-4 bg-gradient-to-br from-purple-500/10 to-indigo-500/10 border border-purple-500/30 rounded-lg">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h4 className="text-lg font-bold text-white flex items-center">
|
|
<span className="w-6 h-6 bg-gradient-to-br from-purple-400 to-purple-600 rounded-lg flex items-center justify-center mr-2 text-sm">
|
|
📸
|
|
</span>
|
|
Chart Screenshots
|
|
</h4>
|
|
<div className="text-xs text-gray-400">
|
|
{sortedData.length} captured • Click to enlarge
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{sortedData.map((item, displayIndex) => {
|
|
const imageUrl = formatScreenshotUrl(item.screenshot)
|
|
|
|
return (
|
|
<div
|
|
key={displayIndex}
|
|
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
|
|
onClick={() => onImageClick(imageUrl)}
|
|
>
|
|
{/* Preview Image */}
|
|
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
|
|
<img
|
|
src={imageUrl}
|
|
alt={`${symbol} - ${item.timeframe} chart`}
|
|
className="w-full h-full object-cover"
|
|
onError={(e: any) => {
|
|
const target = e.target as HTMLImageElement
|
|
target.style.display = 'none'
|
|
const fallback = target.nextElementSibling as HTMLElement
|
|
if (fallback) fallback.classList.remove('hidden')
|
|
}}
|
|
/>
|
|
<div className="hidden absolute inset-0 flex items-center justify-center text-gray-400">
|
|
<div className="text-center">
|
|
<div className="text-3xl mb-2">📊</div>
|
|
<div className="text-sm">Chart Preview</div>
|
|
<div className="text-xs text-gray-500">{item.filename}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overlay */}
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all flex items-center justify-center">
|
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<div className="w-12 h-12 bg-purple-500/80 rounded-full flex items-center justify-center">
|
|
<span className="text-white text-xl">🔍</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Image Info */}
|
|
<div className="p-3">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm font-medium text-white">{symbol}</div>
|
|
<div className="text-xs text-purple-300">{item.timeframe} Timeframe</div>
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
Click to view
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Enlarged Image Modal */}
|
|
{enlargedImage && (
|
|
<div
|
|
className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
|
onClick={onClose}
|
|
>
|
|
<div className="relative max-w-6xl max-h-[90vh] w-full" onClick={(e: any) => e.stopPropagation()}>
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 w-10 h-10 bg-black/50 hover:bg-black/70 rounded-full flex items-center justify-center text-white z-10 transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
|
|
{/* Image */}
|
|
<img
|
|
src={enlargedImage}
|
|
alt="Enlarged chart"
|
|
className="w-full h-full object-contain rounded-lg border border-gray-600"
|
|
onError={(e: any) => {
|
|
console.error('Failed to load enlarged image:', enlargedImage)
|
|
const target = e.target as HTMLImageElement
|
|
target.alt = 'Failed to load image'
|
|
}}
|
|
/>
|
|
|
|
{/* Image Info Overlay */}
|
|
<div className="absolute bottom-4 left-4 right-4 bg-black/70 backdrop-blur-sm rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-white font-medium">{symbol} Chart Analysis</div>
|
|
<div className="text-gray-300 text-sm">AI analyzed screenshot • High resolution view</div>
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
ESC to close • Click outside to close
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|