File size: 6,449 Bytes
9cb3002
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import React, { useEffect, useMemo, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';

const buildPath = (points, width, height) => {
  if (!points.length) {
    return '';
  }

  const values = points.map((point) => point.value);
  const min = Math.min(...values);
  const max = Math.max(...values);
  const range = max - min || 1;

  return points
    .map((point, index) => {
      const x = (index / Math.max(points.length - 1, 1)) * width;
      const y = height - ((point.value - min) / range) * height;
      return `${index === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`;
    })
    .join(' ');
};

export const BalancePanel = ({ portfolio, history, lastDelta, trade }) => {
  const { value = 100000, cash = 100000, positions = {} } = portfolio || {};
  const [displayValue, setDisplayValue] = useState(value);
  const direction = lastDelta > 0 ? 'up' : lastDelta < 0 ? 'down' : 'flat';

  useEffect(() => {
    const start = displayValue;
    const end = value;
    let frameId;
    let startTime;

    const tick = (timestamp) => {
      if (!startTime) {
        startTime = timestamp;
      }
      const progress = Math.min((timestamp - startTime) / 500, 1);
      setDisplayValue(start + (end - start) * progress);
      if (progress < 1) {
        frameId = window.requestAnimationFrame(tick);
      }
    };

    frameId = window.requestAnimationFrame(tick);
    return () => window.cancelAnimationFrame(frameId);
  }, [value]);

  const path = useMemo(() => buildPath(history || [], 260, 86), [history]);
  const invested = Math.max(value - cash, 0);
  const exposure = value > 0 ? (invested / value) * 100 : 0;

  return (
    <div className="relative overflow-hidden rounded-[2rem] border border-[#7d5a4f]/15 bg-[#fff8ef]/92 p-6 shadow-[0_28px_50px_rgba(77,44,26,0.12)]">
      <div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(123,192,255,0.18),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(242,140,111,0.12),transparent_30%)]" />

      <div className="relative z-10">
        <div className="flex items-start justify-between gap-4">
          <div>
            <div className="text-[11px] font-semibold uppercase tracking-[0.34em] text-stone-500">Balance Tape</div>
            <h3 className="mt-2 text-2xl font-semibold text-stone-800">Portfolio Value</h3>
          </div>
          <div
            className={`rounded-full px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.26em] ${
              direction === 'up'
                ? 'bg-emerald-100 text-emerald-700'
                : direction === 'down'
                  ? 'bg-rose-100 text-rose-700'
                  : 'bg-stone-200 text-stone-600'
            }`}
          >
            {trade?.side || 'HOLD'}
          </div>
        </div>

        <motion.div
          key={value}
          animate={direction === 'flat' ? { scale: 1 } : { scale: [1, 1.04, 1] }}
          transition={{ duration: 0.45 }}
          className={`mt-5 text-5xl font-semibold tracking-tight tabular-nums ${
            direction === 'up'
              ? 'text-emerald-600'
              : direction === 'down'
                ? 'text-rose-600'
                : 'text-stone-800'
          }`}
        >
          ${displayValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
        </motion.div>

        <div className="mt-3 flex items-center gap-3 text-sm">
          <span className="rounded-full bg-white/85 px-3 py-1 text-stone-600 shadow-sm">
            Cash ${cash.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
          </span>
          <span className="rounded-full bg-white/85 px-3 py-1 text-stone-600 shadow-sm">
            Exposure {exposure.toFixed(1)}%
          </span>
        </div>

        <div className="mt-6 rounded-[1.6rem] border border-white/70 bg-white/65 p-4">
          <div className="mb-3 flex items-center justify-between text-[11px] font-semibold uppercase tracking-[0.24em] text-stone-500">
            <span>Live equity trail</span>
            <span>
              {lastDelta >= 0 ? '+' : ''}
              {lastDelta.toFixed(2)}
            </span>
          </div>

          <svg viewBox="0 0 260 86" className="h-[86px] w-full overflow-visible">
            <path d={path} fill="none" stroke="#f0d8c7" strokeWidth="10" strokeLinecap="round" />
            <motion.path
              d={path}
              fill="none"
              stroke={direction === 'down' ? '#d95d5d' : '#52b788'}
              strokeWidth="5"
              strokeLinecap="round"
              initial={{ pathLength: 0 }}
              animate={{ pathLength: 1 }}
              transition={{ duration: 0.55 }}
            />
          </svg>
        </div>

        <div className="mt-5 grid grid-cols-2 gap-3">
          <div className="rounded-[1.4rem] bg-white/70 px-4 py-4">
            <div className="text-[10px] uppercase tracking-[0.24em] text-stone-500">Invested</div>
            <div className="mt-2 text-lg font-semibold text-stone-800">
              ${invested.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
            </div>
          </div>
          <div className="rounded-[1.4rem] bg-white/70 px-4 py-4">
            <div className="text-[10px] uppercase tracking-[0.24em] text-stone-500">Open books</div>
            <div className="mt-2 text-lg font-semibold text-stone-800">{Object.keys(positions || {}).length}</div>
          </div>
        </div>
      </div>

      <AnimatePresence>
        {direction !== 'flat' && (
          <>
            {[0, 1, 2].map((index) => (
              <motion.div
                // eslint-disable-next-line react/no-array-index-key
                key={`${trade?.pulse || 0}-${direction}-${index}`}
                className={`pointer-events-none absolute bottom-10 left-[12%] h-4 w-4 rounded-full ${
                  direction === 'up' ? 'bg-emerald-400/60' : 'bg-rose-400/55'
                }`}
                initial={{ y: 0, x: index * 24, opacity: 0.8, scale: 0.8 }}
                animate={{ y: direction === 'up' ? -88 : 52, x: index * 56 + 24, opacity: 0, scale: 1.45 }}
                exit={{ opacity: 0 }}
                transition={{ duration: 1.15, delay: index * 0.08, ease: 'easeOut' }}
              />
            ))}
          </>
        )}
      </AnimatePresence>
    </div>
  );
};