File size: 4,164 Bytes
5a81b95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { useEffect, useState } from 'react';
import { Maximize2 } from 'lucide-react';
import SourceLink, { DataSource } from './SourceLink';
import { DrillDownModal, DrillDownData } from './DrillDownModal';

interface DataPanelProps {
  title: string;
  value: string | number;
  subtitle?: string;
  trend?: 'up' | 'down' | 'stable';
  animate?: boolean;
  source?: DataSource;
  /** Enable drill-down capability */
  drillDown?: DrillDownData;
  /** Backend endpoint for fetching detailed data */
  drillDownEndpoint?: string;
}

const DataPanel = ({ title, value, subtitle, trend, animate = true, source, drillDown, drillDownEndpoint }: DataPanelProps) => {
  const [displayValue, setDisplayValue] = useState(animate ? '---' : value);
  const [showDrillDown, setShowDrillDown] = useState(false);

  // Build drill-down data from props
  const drillDownData: DrillDownData = drillDown || {
    id: title.toLowerCase().replace(/\s+/g, '-'),
    title: title,
    description: subtitle,
    endpoint: drillDownEndpoint,
    metrics: [
      { label: 'Nuværende værdi', value: String(value), trend }
    ],
    details: {
      title,
      value,
      subtitle,
      trend,
      source: source?.name
    }
  };

  const hasDrillDown = drillDown || drillDownEndpoint;

  useEffect(() => {
    if (!animate) return;
    
    const chars = '0123456789ABCDEF';
    let iterations = 0;
    const maxIterations = 10;
    
    const interval = setInterval(() => {
      if (iterations >= maxIterations) {
        setDisplayValue(value);
        clearInterval(interval);
        return;
      }
      
      setDisplayValue(
        String(value)
          .split('')
          .map((char, index) => {
            if (index < iterations) return char;
            return chars[Math.floor(Math.random() * chars.length)];
          })
          .join('')
      );
      
      iterations++;
    }, 50);

    return () => clearInterval(interval);
  }, [value, animate]);

  return (
    <>
      <div
        className={`group relative p-4 bg-secondary/30 border border-border/30 hover:border-primary/50 transition-all duration-300 ${hasDrillDown ? 'cursor-pointer' : ''}`}
        onClick={hasDrillDown ? () => setShowDrillDown(true) : undefined}
      >
        <div className="flex items-start justify-between mb-2">
          <span className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
            {title}
          </span>
          <div className="flex items-center gap-2">
            {hasDrillDown && (
              <Maximize2 className="h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
            )}
            {source && <SourceLink source={source} variant="icon" />}
            {trend && (
              <span className={`text-xs ${
                trend === 'up' ? 'text-primary' :
                trend === 'down' ? 'text-destructive' :
                'text-muted-foreground'
              }`}>
                {trend === 'up' ? '▲' : trend === 'down' ? '▼' : '●'}
              </span>
            )}
          </div>
        </div>
        <div className="font-display text-2xl sm:text-3xl font-bold text-foreground group-hover:text-primary transition-colors">
          {displayValue}
        </div>
        {subtitle && (
          <div className="mt-1 flex items-center justify-between gap-2">
            <p className="font-mono text-xs text-muted-foreground">
              {subtitle}
            </p>
            {source && <SourceLink source={source} variant="text" />}
          </div>
        )}

        {/* Scan line on hover */}
        <div className="absolute inset-0 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">
          <div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary to-transparent animate-scan" />
        </div>
      </div>

      {/* Drill-Down Modal */}
      {hasDrillDown && (
        <DrillDownModal
          isOpen={showDrillDown}
          onClose={() => setShowDrillDown(false)}
          data={drillDownData}
        />
      )}
    </>
  );
};

export default DataPanel;