feat: comprehensive FAQ rewrite with navigation fix

- Add HeaderBar to FAQ page for proper navigation
- Rewrite FAQ with 9 categories and ~50 Q&A items
- Add EN/ZH translations for all FAQ content
- Remove China-specific references
- Update AI model names (GPT-5.2)
- Update description to include crypto and US stock markets
This commit is contained in:
tinkle-community
2025-12-14 23:12:09 +08:00
parent da1d905a31
commit f901f954d9
3 changed files with 896 additions and 527 deletions

View File

@@ -59,18 +59,26 @@ function getModelDisplayName(modelId: string): string {
}
// Helper function to get exchange display name from exchange ID (UUID)
function getExchangeDisplayNameFromList(exchangeId: string | undefined, exchanges: Exchange[] | undefined): string {
function getExchangeDisplayNameFromList(
exchangeId: string | undefined,
exchanges: Exchange[] | undefined
): string {
if (!exchangeId) return 'Unknown'
const exchange = exchanges?.find(e => e.id === exchangeId)
const exchange = exchanges?.find((e) => e.id === exchangeId)
if (!exchange) return exchangeId.substring(0, 8).toUpperCase() + '...'
const typeName = exchange.exchange_type?.toUpperCase() || exchange.name
return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName
return exchange.account_name
? `${typeName} - ${exchange.account_name}`
: typeName
}
// Helper function to get exchange type from exchange ID (UUID) - for TradingView charts
function getExchangeTypeFromList(exchangeId: string | undefined, exchanges: Exchange[] | undefined): string {
function getExchangeTypeFromList(
exchangeId: string | undefined,
exchanges: Exchange[] | undefined
): string {
if (!exchangeId) return 'BINANCE'
const exchange = exchanges?.find(e => e.id === exchangeId)
const exchange = exchanges?.find((e) => e.id === exchangeId)
if (!exchange) return 'BINANCE' // Default to BINANCE for charts
return exchange.exchange_type?.toUpperCase() || 'BINANCE'
}
@@ -288,7 +296,52 @@ function App() {
return <RegisterPage />
}
if (route === '/faq') {
return <FAQPage />
return (
<div
className="min-h-screen"
style={{ background: '#0B0E11', color: '#EAECEF' }}
>
<HeaderBar
isLoggedIn={!!user}
currentPage="faq"
language={language}
onLanguageChange={setLanguage}
user={user}
onLogout={logout}
onPageChange={(page: Page) => {
if (page === 'competition') {
window.history.pushState({}, '', '/competition')
setRoute('/competition')
setCurrentPage('competition')
} else if (page === 'traders') {
window.history.pushState({}, '', '/traders')
setRoute('/traders')
setCurrentPage('traders')
} else if (page === 'trader') {
window.history.pushState({}, '', '/dashboard')
setRoute('/dashboard')
setCurrentPage('trader')
} else if (page === 'faq') {
window.history.pushState({}, '', '/faq')
setRoute('/faq')
} else if (page === 'backtest') {
window.history.pushState({}, '', '/backtest')
setRoute('/backtest')
setCurrentPage('backtest')
} else if (page === 'strategy') {
window.history.pushState({}, '', '/strategy')
setRoute('/strategy')
setCurrentPage('strategy')
} else if (page === 'debate') {
window.history.pushState({}, '', '/debate')
setRoute('/debate')
setCurrentPage('debate')
}
}}
/>
<FAQPage />
</div>
)
}
if (route === '/reset-password') {
return <ResetPasswordPage />
@@ -453,7 +506,13 @@ function App() {
/>
{/* Main Content */}
<main className={currentPage === 'debate' ? 'h-[calc(100vh-64px)] mt-16' : 'max-w-[1920px] mx-auto px-6 py-6 pt-24'}>
<main
className={
currentPage === 'debate'
? 'h-[calc(100vh-64px)] mt-16'
: 'max-w-[1920px] mx-auto px-6 py-6 pt-24'
}
>
{currentPage === 'competition' ? (
<CompetitionPage />
) : currentPage === 'traders' ? (
@@ -498,101 +557,118 @@ function App() {
</main>
{/* Footer - Hidden on debate page */}
{currentPage !== 'debate' && <footer
className="mt-16"
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
>
<div
className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm"
style={{ color: '#5E6673' }}
{currentPage !== 'debate' && (
<footer
className="mt-16"
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
>
<p>{t('footerTitle', language)}</p>
<p className="mt-1">{t('footerWarning', language)}</p>
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
{/* GitHub */}
<a
href={OFFICIAL_LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#F0B90B'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
GitHub
</a>
{/* Twitter/X */}
<a
href={OFFICIAL_LINKS.twitter}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#1DA1F2'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Twitter
</a>
{/* Telegram */}
<a
href={OFFICIAL_LINKS.telegram}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#0088cc'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
Telegram
</a>
<div
className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm"
style={{ color: '#5E6673' }}
>
<p>{t('footerTitle', language)}</p>
<p className="mt-1">{t('footerWarning', language)}</p>
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
{/* GitHub */}
<a
href={OFFICIAL_LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#F0B90B'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg
width="18"
height="18"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
GitHub
</a>
{/* Twitter/X */}
<a
href={OFFICIAL_LINKS.twitter}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#1DA1F2'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Twitter
</a>
{/* Telegram */}
<a
href={OFFICIAL_LINKS.telegram}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#0088cc'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
}}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
Telegram
</a>
</div>
</div>
</div>
</footer>}
</footer>
)}
</div>
)
}
@@ -633,7 +709,9 @@ function TraderDetailsPage({
exchanges?: Exchange[]
}) {
const [closingPosition, setClosingPosition] = useState<string | null>(null)
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>(undefined)
const [selectedChartSymbol, setSelectedChartSymbol] = useState<
string | undefined
>(undefined)
const [chartUpdateKey, setChartUpdateKey] = useState<number>(0)
const chartSectionRef = useRef<HTMLDivElement>(null)
@@ -641,9 +719,10 @@ function TraderDetailsPage({
const handleClosePosition = async (symbol: string, side: string) => {
if (!selectedTraderId) return
const confirmMsg = language === 'zh'
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
const confirmMsg =
language === 'zh'
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
const confirmed = await confirmToast(confirmMsg, {
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
@@ -656,14 +735,21 @@ function TraderDetailsPage({
setClosingPosition(symbol)
try {
await api.closePosition(selectedTraderId, symbol, side)
notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully')
notify.success(
language === 'zh' ? '平仓成功' : 'Position closed successfully'
)
// 使用 SWR mutate 刷新数据而非重新加载页面
await Promise.all([
mutate(`positions-${selectedTraderId}`),
mutate(`account-${selectedTraderId}`),
])
} catch (err: unknown) {
const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position')
const errorMsg =
err instanceof Error
? err.message
: language === 'zh'
? '平仓失败'
: 'Failed to close position'
notify.error(errorMsg)
} finally {
setClosingPosition(null)
@@ -828,7 +914,10 @@ function TraderDetailsPage({
style={{ color: '#EAECEF' }}
>
<PunkAvatar
seed={getTraderAvatar(selectedTrader.trader_id, selectedTrader.trader_name)}
seed={getTraderAvatar(
selectedTrader.trader_id,
selectedTrader.trader_name
)}
size={48}
className="rounded-lg"
/>
@@ -876,7 +965,7 @@ function TraderDetailsPage({
>
{getModelDisplayName(
selectedTrader.ai_model.split('_').pop() ||
selectedTrader.ai_model
selectedTrader.ai_model
)}
</span>
</span>
@@ -884,7 +973,10 @@ function TraderDetailsPage({
<span>
Exchange:{' '}
<span className="font-semibold" style={{ color: '#EAECEF' }}>
{getExchangeDisplayNameFromList(selectedTrader.exchange_id, exchanges)}
{getExchangeDisplayNameFromList(
selectedTrader.exchange_id,
exchanges
)}
</span>
</span>
<span></span>
@@ -961,7 +1053,10 @@ function TraderDetailsPage({
traderId={selectedTrader.trader_id}
selectedSymbol={selectedChartSymbol}
updateKey={chartUpdateKey}
exchangeId={getExchangeTypeFromList(selectedTrader.exchange_id, exchanges)}
exchangeId={getExchangeTypeFromList(
selectedTrader.exchange_id,
exchanges
)}
/>
</div>
@@ -1004,25 +1099,46 @@ function TraderDetailsPage({
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
{language === 'zh' ? '操作' : 'Action'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('entryPrice', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
title={t('entryPrice', language)}
>
{language === 'zh' ? '入场价' : 'Entry'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('markPrice', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
title={t('markPrice', language)}
>
{language === 'zh' ? '标记价' : 'Mark'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('quantity', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
title={t('quantity', language)}
>
{language === 'zh' ? '数量' : 'Qty'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('positionValue', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
title={t('positionValue', language)}
>
{language === 'zh' ? '价值' : 'Value'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center" title={t('leverage', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center"
title={t('leverage', language)}
>
{language === 'zh' ? '杠杆' : 'Lev.'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
title={t('unrealizedPnL', language)}
>
{language === 'zh' ? '未实现盈亏' : 'uPnL'}
</th>
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right" title={t('liqPrice', language)}>
<th
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
title={t('liqPrice', language)}
>
{language === 'zh' ? '强平价' : 'Liq.'}
</th>
</tr>
@@ -1037,7 +1153,10 @@ function TraderDetailsPage({
setChartUpdateKey(Date.now())
// Smooth scroll to chart with ref
if (chartSectionRef.current) {
chartSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' })
chartSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
})
}
}}
>
@@ -1050,13 +1169,13 @@ function TraderDetailsPage({
style={
pos.side === 'long'
? {
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
}
background: 'rgba(14, 203, 129, 0.1)',
color: '#0ECB81',
}
: {
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
}
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
}
}
>
{t(
@@ -1069,12 +1188,17 @@ function TraderDetailsPage({
<button
type="button"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
handleClosePosition(pos.symbol, pos.side.toUpperCase())
e.stopPropagation() // Prevent row click
handleClosePosition(
pos.symbol,
pos.side.toUpperCase()
)
}}
disabled={closingPosition === pos.symbol}
className="btn-danger inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto"
title={language === 'zh' ? '平仓' : 'Close Position'}
title={
language === 'zh' ? '平仓' : 'Close Position'
}
>
{closingPosition === pos.symbol ? (
<Loader2 className="w-3 h-3 animate-spin" />
@@ -1227,7 +1351,6 @@ function TraderDetailsPage({
</div>
{/* 右侧结束 */}
</div>
</div>
)
}