File size: 12,149 Bytes
b72deed
ee37d63
b72deed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee37d63
 
b72deed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
facb33b
b72deed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43c1680
 
 
 
 
 
 
 
 
ee37d63
 
 
 
 
 
 
 
 
e5c0ab7
 
 
 
 
 
 
 
 
b72deed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import React, { useState, useRef, useEffect, useCallback } from 'react'
import { Search, TrendingUp, Loader2, LogOut, Star, BarChart2, User, X, Gem, ScanLine, Droplets } from 'lucide-react'
import { searchStocks, SearchResult } from '../api/stockApi'

export type Market = 'tw' | 'us' | 'fund'

const MARKET_TABS: { key: Market; label: string; flag: string }[] = [
  { key: 'tw',   label: '台肑', flag: 'πŸ‡ΉπŸ‡Ό' },
  { key: 'us',   label: 'ηΎŽθ‚‘', flag: 'πŸ‡ΊπŸ‡Έ' },
  { key: 'fund', label: 'εŸΊι‡‘', flag: 'πŸ“Š' },
]

const MARKET_PLACEHOLDER: Record<Market, string> = {
  tw:   'ε°θ‚‘δ»£θ™Ÿ (e.g. 7856)',
  us:   'ηΎŽθ‚‘δ»£θ™Ÿ (e.g. AAPL)',
  fund: 'εŸΊι‡‘δ»£θ™Ÿ (e.g. PIM91)',
}

interface HeaderProps {
  input: string
  setInput: (val: string) => void
  months: number
  setMonths: (val: number) => void
  loading: boolean
  onSubmit: (e: React.FormEvent) => void
  onLogout: () => void
  currentUser: string
  userPhotoURL?: string
  view: 'main' | 'favorites' | 'metals' | 'oil' | 'recommend'
  onViewChange: (v: 'main' | 'favorites' | 'metals' | 'oil' | 'recommend') => void
  favoritesCount: number
  market: Market
  onMarketChange: (m: Market) => void
  onSelect: (stockNo: string) => void
}

export const Header: React.FC<HeaderProps> = ({
  input, setInput,
  months, setMonths,
  loading,
  onSubmit,
  onLogout,
  currentUser,
  userPhotoURL,
  view, onViewChange,
  favoritesCount,
  market, onMarketChange,
  onSelect,
}) => {
  const [suggestions, setSuggestions] = useState<SearchResult[]>([])
  const [showDrop, setShowDrop] = useState(false)
  const [searching, setSearching] = useState(false)
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const wrapperRef = useRef<HTMLDivElement>(null)

  // Close dropdown when clicking outside
  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
        setShowDrop(false)
      }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [])

  const fetchSuggestions = useCallback(async (q: string, mkt: Market) => {
    if (!q.trim()) { setSuggestions([]); setShowDrop(false); return }
    // For TW: need at least 1 char; for US/fund: 1 char
    if (q.length < 1) return
    setSearching(true)
    try {
      const results = await searchStocks(q, mkt)
      setSuggestions(results)
      setShowDrop(results.length > 0)
    } catch {
      setSuggestions([])
      setShowDrop(false)
    } finally {
      setSearching(false)
    }
  }, [])

  const handleInputChange = (val: string) => {
    setInput(val)
    if (debounceRef.current) clearTimeout(debounceRef.current)
    debounceRef.current = setTimeout(() => fetchSuggestions(val, market), 350)
  }

  const handleSelect = (stockNo: string) => {
    setInput(stockNo)
    setSuggestions([])
    setShowDrop(false)
    onSelect(stockNo)
  }

  const handleMarketChange = (m: Market) => {
    onMarketChange(m)
    setSuggestions([])
    setShowDrop(false)
    setInput('')
  }

  const handleFormSubmit = (e: React.FormEvent) => {
    setShowDrop(false)
    onSubmit(e)
  }

  const exchangeColor: Record<string, string> = {
    TWSE: 'text-blue-400', TPEX: 'text-purple-400',
    εŸΊι‡‘: 'text-yellow-400', MoneyDJ: 'text-yellow-400',
    NYSE: 'text-green-400', NASDAQ: 'text-green-400',
  }

  return (
    <header className="bg-slate-900 border-b border-slate-800 sticky top-0 z-20">
      <div className="max-w-7xl mx-auto px-4 py-3 flex flex-col sm:flex-row items-start sm:items-center gap-3">

        {/* Brand */}
        <div className="flex items-center gap-2 mr-2 flex-shrink-0">
          <TrendingUp className="text-blue-400" size={22} />
          <h1 className="text-lg font-bold tracking-tight text-white whitespace-nowrap hidden sm:block">ML θ‚‘η₯¨ι ζΈ¬</h1>
        </div>

        {/* View nav (εˆ†ζž / ζœ€ζ„›) */}
        <nav className="flex items-center gap-1 flex-shrink-0">
          <button
            onClick={() => onViewChange('main')}
            className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
              view === 'main' ? 'bg-blue-600/20 text-blue-400' : 'text-slate-400 hover:text-white hover:bg-slate-800'
            }`}
          >
            <BarChart2 size={14} /> εˆ†ζž
          </button>
          <button
            onClick={() => onViewChange('favorites')}
            className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
              view === 'favorites' ? 'bg-yellow-500/20 text-yellow-400' : 'text-slate-400 hover:text-white hover:bg-slate-800'
            }`}
          >
            <Star size={14} fill={view === 'favorites' ? 'currentColor' : 'none'} />
            ζœ€ζ„›
            {favoritesCount > 0 && (
              <span className="bg-yellow-500 text-black text-[10px] font-bold rounded-full w-4 h-4 flex items-center justify-center leading-none">
                {favoritesCount > 9 ? '9+' : favoritesCount}
              </span>
            )}
          </button>
          <button
            onClick={() => onViewChange('metals')}
            className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
              view === 'metals' ? 'bg-yellow-500/20 text-yellow-400' : 'text-slate-400 hover:text-white hover:bg-slate-800'
            }`}
          >
            <Gem size={14} />
            貴金屬
          </button>
          <button
            onClick={() => onViewChange('oil')}
            className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
              view === 'oil' ? 'bg-cyan-500/20 text-cyan-300' : 'text-slate-400 hover:text-white hover:bg-slate-800'
            }`}
          >
            <Droplets size={14} />
            石油
          </button>
          <button
            onClick={() => onViewChange('recommend')}
            className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
              view === 'recommend' ? 'bg-blue-600/20 text-blue-400' : 'text-slate-400 hover:text-white hover:bg-slate-800'
            }`}
          >
            <ScanLine size={14} />
            Market Scan
          </button>
        </nav>

        {/* Market tabs + search (only on main view) */}
        {view === 'main' && (
          <div className="flex items-center gap-2 flex-1">
            {/* Market tabs */}
            <div className="flex items-center bg-slate-800 rounded-lg p-0.5 flex-shrink-0">
              {MARKET_TABS.map(tab => (
                <button
                  key={tab.key}
                  onClick={() => handleMarketChange(tab.key)}
                  className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${
                    market === tab.key
                      ? 'bg-slate-700 text-white shadow-sm'
                      : 'text-slate-400 hover:text-white'
                  }`}
                >
                  <span>{tab.flag}</span>
                  <span className="hidden sm:inline">{tab.label}</span>
                </button>
              ))}
            </div>

            {/* Search with autocomplete */}
            <form onSubmit={handleFormSubmit} className="flex items-center gap-2 flex-1">
              <div className="relative flex-1 sm:flex-none" ref={wrapperRef}>
                <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none z-10" />
                <input
                  type="text"
                  value={input}
                  onChange={(e) => handleInputChange(e.target.value)}
                  onFocus={() => suggestions.length > 0 && setShowDrop(true)}
                  placeholder={MARKET_PLACEHOLDER[market]}
                  className="bg-slate-800 border border-slate-600 rounded-lg pl-8 pr-7 py-2 text-sm w-full sm:w-52 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-white placeholder-slate-500"
                />
                {input && (
                  <button
                    type="button"
                    onClick={() => { setInput(''); setSuggestions([]); setShowDrop(false) }}
                    className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-500 hover:text-white"
                  >
                    <X size={12} />
                  </button>
                )}

                {/* Autocomplete dropdown */}
                {showDrop && (
                  <div className="absolute top-full left-0 right-0 mt-1 bg-slate-800 border border-slate-700 rounded-lg shadow-xl z-50 max-h-64 overflow-y-auto">
                    {searching && (
                      <div className="px-3 py-2 text-xs text-slate-500 flex items-center gap-2">
                        <Loader2 size={12} className="animate-spin" /> ζœε°‹δΈ­...
                      </div>
                    )}
                    {suggestions.map((s) => (
                      <button
                        key={s.stock_no}
                        type="button"
                        onClick={() => handleSelect(s.stock_no)}
                        className="w-full px-3 py-2 text-left hover:bg-slate-700 flex items-center justify-between gap-2 transition-colors"
                      >
                        <div className="flex items-center gap-2 min-w-0">
                          <span className="font-mono font-bold text-white text-sm flex-shrink-0">{s.stock_no}</span>
                          <span className="text-slate-300 text-xs truncate">{s.name}</span>
                        </div>
                        <span className={`text-[10px] font-medium flex-shrink-0 ${exchangeColor[s.exchange] ?? 'text-slate-400'}`}>
                          {s.exchange}
                        </span>
                      </button>
                    ))}
                  </div>
                )}
              </div>

              <select
                value={months}
                onChange={(e) => setMonths(Number(e.target.value))}
                className="bg-slate-800 border border-slate-600 rounded-lg px-2 py-2 text-sm focus:outline-none focus:border-blue-500 text-white flex-shrink-0"
              >
                <option value={3}>3ε€‹ζœˆ</option>
                <option value={6}>6ε€‹ζœˆ</option>
                <option value={12}>12ε€‹ζœˆ</option>
                <option value={24}>24ε€‹ζœˆ</option>
              </select>

              <button
                type="submit"
                disabled={loading}
                className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-blue-800 disabled:cursor-not-allowed text-white rounded-lg px-4 py-2 text-sm font-medium transition-colors flex-shrink-0"
              >
                {loading ? <Loader2 size={14} className="animate-spin" /> : <Search size={14} />}
                <span className="hidden sm:inline">ζŸ₯θ©’</span>
              </button>
            </form>
          </div>
        )}

        {/* User avatar + logout */}
        <div className="flex items-center gap-2 sm:ml-auto flex-shrink-0">
          {userPhotoURL ? (
            <img src={userPhotoURL} alt={currentUser} className="w-7 h-7 rounded-full border border-slate-600 object-cover" referrerPolicy="no-referrer" />
          ) : (
            <div className="w-7 h-7 rounded-full bg-slate-700 flex items-center justify-center">
              <User size={14} className="text-slate-400" />
            </div>
          )}
          <span className="hidden sm:inline text-xs font-medium text-slate-300 max-w-[100px] truncate">{currentUser}</span>
          <button
            onClick={onLogout}
            className="p-2 text-slate-400 hover:text-white transition-colors rounded-lg hover:bg-slate-800"
            title="η™»ε‡Ί"
            type="button"
          >
            <LogOut size={18} />
          </button>
        </div>
      </div>
    </header>
  )
}