mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
feat: add Web3 punk avatars and official social links
- Add PunkAvatar component for Web3-style trader avatars - Integrate punk avatars into trader cards and dashboard header - Add official Twitter/Telegram links to footer with anti-fork protection - Create branding.ts with Base64 encoded official links
This commit is contained in:
@@ -18,6 +18,8 @@ import { t, type Language } from './i18n/translations'
|
|||||||
import { confirmToast, notify } from './lib/notify'
|
import { confirmToast, notify } from './lib/notify'
|
||||||
import { useSystemConfig } from './hooks/useSystemConfig'
|
import { useSystemConfig } from './hooks/useSystemConfig'
|
||||||
import { DecisionCard } from './components/DecisionCard'
|
import { DecisionCard } from './components/DecisionCard'
|
||||||
|
import { PunkAvatar, getTraderAvatar } from './components/PunkAvatar'
|
||||||
|
import { OFFICIAL_LINKS } from './constants/branding'
|
||||||
import { BacktestPage } from './components/BacktestPage'
|
import { BacktestPage } from './components/BacktestPage'
|
||||||
import { LogOut, Loader2 } from 'lucide-react'
|
import { LogOut, Loader2 } from 'lucide-react'
|
||||||
import type {
|
import type {
|
||||||
@@ -458,9 +460,10 @@ function App() {
|
|||||||
>
|
>
|
||||||
<p>{t('footerTitle', language)}</p>
|
<p>{t('footerTitle', language)}</p>
|
||||||
<p className="mt-1">{t('footerWarning', language)}</p>
|
<p className="mt-1">{t('footerWarning', language)}</p>
|
||||||
<div className="mt-4">
|
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
|
||||||
|
{/* GitHub */}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/tinkle-community/nofx"
|
href={OFFICIAL_LINKS.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||||
@@ -480,16 +483,65 @@ function App() {
|
|||||||
e.currentTarget.style.borderColor = '#2B3139'
|
e.currentTarget.style.borderColor = '#2B3139'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||||
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" />
|
<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>
|
</svg>
|
||||||
GitHub
|
GitHub
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -718,17 +770,14 @@ function TraderDetailsPage({
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<h2
|
<h2
|
||||||
className="text-2xl font-bold flex items-center gap-2"
|
className="text-2xl font-bold flex items-center gap-3"
|
||||||
style={{ color: '#EAECEF' }}
|
style={{ color: '#EAECEF' }}
|
||||||
>
|
>
|
||||||
<span
|
<PunkAvatar
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-xl"
|
seed={getTraderAvatar(selectedTrader.trader_id, selectedTrader.trader_name)}
|
||||||
style={{
|
size={48}
|
||||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
className="rounded-lg"
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
🤖
|
|
||||||
</span>
|
|
||||||
{selectedTrader.trader_name}
|
{selectedTrader.trader_name}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useAuth } from '../contexts/AuthContext'
|
|||||||
import { getExchangeIcon } from './ExchangeIcons'
|
import { getExchangeIcon } from './ExchangeIcons'
|
||||||
import { getModelIcon } from './ModelIcons'
|
import { getModelIcon } from './ModelIcons'
|
||||||
import { TraderConfigModal } from './TraderConfigModal'
|
import { TraderConfigModal } from './TraderConfigModal'
|
||||||
|
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||||
import {
|
import {
|
||||||
TwoStageKeyModal,
|
TwoStageKeyModal,
|
||||||
type TwoStageKeyModalResult,
|
type TwoStageKeyModalResult,
|
||||||
@@ -980,16 +981,17 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
<div
|
<div className="flex-shrink-0">
|
||||||
className="w-10 h-10 md:w-12 md:h-12 rounded-full flex items-center justify-center flex-shrink-0"
|
<PunkAvatar
|
||||||
style={{
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
background: trader.ai_model.includes('deepseek')
|
size={48}
|
||||||
? '#60a5fa'
|
className="rounded-lg hidden md:block"
|
||||||
: '#c084fc',
|
/>
|
||||||
color: '#fff',
|
<PunkAvatar
|
||||||
}}
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
>
|
size={40}
|
||||||
<Bot className="w-5 h-5 md:w-6 md:h-6" />
|
className="rounded-lg md:hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Trophy, Medal } from 'lucide-react'
|
import { Trophy } from 'lucide-react'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import type { CompetitionData } from '../types'
|
import type { CompetitionData } from '../types'
|
||||||
@@ -8,6 +8,7 @@ import { TraderConfigViewModal } from './TraderConfigViewModal'
|
|||||||
import { getTraderColor } from '../utils/traderColors'
|
import { getTraderColor } from '../utils/traderColors'
|
||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { t } from '../i18n/translations'
|
import { t } from '../i18n/translations'
|
||||||
|
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||||
|
|
||||||
export function CompetitionPage() {
|
export function CompetitionPage() {
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
@@ -259,21 +260,30 @@ export function CompetitionPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Rank & Name */}
|
{/* Rank & Avatar & Name */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-6 flex items-center justify-center">
|
{/* Rank Badge */}
|
||||||
<Medal
|
<div
|
||||||
className="w-5 h-5"
|
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||||
style={{
|
style={{
|
||||||
color:
|
background: index === 0
|
||||||
index === 0
|
? 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)'
|
||||||
? '#F0B90B'
|
: index === 1
|
||||||
: index === 1
|
? 'linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%)'
|
||||||
? '#C0C0C0'
|
: index === 2
|
||||||
: '#CD7F32',
|
? 'linear-gradient(135deg, #CD7F32 0%, #E8A64C 100%)'
|
||||||
}}
|
: '#2B3139',
|
||||||
/>
|
color: index < 3 ? '#000' : '#848E9C',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Punk Avatar */}
|
||||||
|
<PunkAvatar
|
||||||
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
|
size={36}
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="font-bold text-sm"
|
className="font-bold text-sm"
|
||||||
@@ -424,6 +434,14 @@ export function CompetitionPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="flex justify-center mb-3">
|
||||||
|
<PunkAvatar
|
||||||
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
|
size={56}
|
||||||
|
className="rounded-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-sm md:text-base font-bold mb-2"
|
className="text-sm md:text-base font-bold mb-2"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
343
web/src/components/PunkAvatar.tsx
Normal file
343
web/src/components/PunkAvatar.tsx
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
interface PunkAvatarProps {
|
||||||
|
seed: string
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash function to generate consistent random values from seed
|
||||||
|
function hashCode(str: string): number {
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i)
|
||||||
|
hash = ((hash << 5) - hash) + char
|
||||||
|
hash = hash & hash
|
||||||
|
}
|
||||||
|
return Math.abs(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a value from hash at specific position
|
||||||
|
function getHashValue(hash: number, position: number, max: number): number {
|
||||||
|
return ((hash >> (position * 4)) & 0xF) % max
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color palettes - Web3/Crypto aesthetic
|
||||||
|
const BACKGROUNDS = [
|
||||||
|
'#1a1a2e', '#16213e', '#0f3460', '#1b1b2f', '#162447',
|
||||||
|
'#1f1f3d', '#2d132c', '#1e1e3f', '#0d1b2a', '#1b263b',
|
||||||
|
'#252538', '#2a2a4a', '#1e2a3a', '#0f172a', '#1a1f35',
|
||||||
|
]
|
||||||
|
|
||||||
|
const SKIN_TONES = [
|
||||||
|
'#ffd5c8', '#f5c5b5', '#daa06d', '#c68642', '#8d5524',
|
||||||
|
'#6b4423', '#4a3728', '#ffdbac', '#f1c27d', '#e0ac69',
|
||||||
|
]
|
||||||
|
|
||||||
|
const HAIR_COLORS = [
|
||||||
|
'#090806', '#2c222b', '#3b3024', '#4a4035', '#504444',
|
||||||
|
'#6a4e42', '#a55728', '#b55239', '#8d4a43', '#91553d',
|
||||||
|
'#e6cea8', '#e5c8a8', '#debc99', '#977961', '#343434',
|
||||||
|
'#9a3300', '#ff6b6b', '#4ecdc4', '#ffe66d', '#a855f7',
|
||||||
|
]
|
||||||
|
|
||||||
|
const ACCESSORY_COLORS = [
|
||||||
|
'#F0B90B', '#0ECB81', '#F6465D', '#60a5fa', '#a855f7',
|
||||||
|
'#ec4899', '#14b8a6', '#f97316', '#84cc16', '#06b6d4',
|
||||||
|
]
|
||||||
|
|
||||||
|
export function PunkAvatar({ seed, size = 40, className = '' }: PunkAvatarProps) {
|
||||||
|
const avatar = useMemo(() => {
|
||||||
|
const hash = hashCode(seed)
|
||||||
|
|
||||||
|
// Deterministic selections based on hash
|
||||||
|
const bgColor = BACKGROUNDS[getHashValue(hash, 0, BACKGROUNDS.length)]
|
||||||
|
const skinColor = SKIN_TONES[getHashValue(hash, 1, SKIN_TONES.length)]
|
||||||
|
const hairColor = HAIR_COLORS[getHashValue(hash, 2, HAIR_COLORS.length)]
|
||||||
|
const accColor = ACCESSORY_COLORS[getHashValue(hash, 3, ACCESSORY_COLORS.length)]
|
||||||
|
|
||||||
|
const hairStyle = getHashValue(hash, 4, 8)
|
||||||
|
const eyeStyle = getHashValue(hash, 5, 6)
|
||||||
|
const mouthStyle = getHashValue(hash, 6, 5)
|
||||||
|
const hasGlasses = getHashValue(hash, 7, 4) === 0
|
||||||
|
const hasEarring = getHashValue(hash, 8, 5) === 0
|
||||||
|
const hasMask = getHashValue(hash, 9, 8) === 0
|
||||||
|
const hasLaser = getHashValue(hash, 10, 12) === 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
bgColor,
|
||||||
|
skinColor,
|
||||||
|
hairColor,
|
||||||
|
accColor,
|
||||||
|
hairStyle,
|
||||||
|
eyeStyle,
|
||||||
|
mouthStyle,
|
||||||
|
hasGlasses,
|
||||||
|
hasEarring,
|
||||||
|
hasMask,
|
||||||
|
hasLaser,
|
||||||
|
}
|
||||||
|
}, [seed])
|
||||||
|
|
||||||
|
// Pixel size for 24x24 grid
|
||||||
|
const px = size / 24
|
||||||
|
|
||||||
|
const renderHair = () => {
|
||||||
|
const { hairColor, hairStyle } = avatar
|
||||||
|
switch (hairStyle) {
|
||||||
|
case 0: // Mohawk
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={11*px} y={2*px} width={2*px} height={5*px} fill={hairColor} />
|
||||||
|
<rect x={10*px} y={3*px} width={4*px} height={1*px} fill={hairColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 1: // Messy
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={7*px} y={4*px} width={10*px} height={3*px} fill={hairColor} />
|
||||||
|
<rect x={8*px} y={3*px} width={8*px} height={1*px} fill={hairColor} />
|
||||||
|
<rect x={6*px} y={5*px} width={2*px} height={2*px} fill={hairColor} />
|
||||||
|
<rect x={16*px} y={5*px} width={2*px} height={2*px} fill={hairColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 2: // Cap
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={6*px} y={5*px} width={12*px} height={3*px} fill={avatar.accColor} />
|
||||||
|
<rect x={5*px} y={7*px} width={14*px} height={1*px} fill={avatar.accColor} />
|
||||||
|
<rect x={7*px} y={4*px} width={10*px} height={1*px} fill={avatar.accColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 3: // Long
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={7*px} y={4*px} width={10*px} height={4*px} fill={hairColor} />
|
||||||
|
<rect x={6*px} y={6*px} width={2*px} height={8*px} fill={hairColor} />
|
||||||
|
<rect x={16*px} y={6*px} width={2*px} height={8*px} fill={hairColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 4: // Bald with shine
|
||||||
|
return (
|
||||||
|
<rect x={9*px} y={5*px} width={2*px} height={1*px} fill="rgba(255,255,255,0.3)" />
|
||||||
|
)
|
||||||
|
case 5: // Spiky
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={7*px} y={5*px} width={10*px} height={2*px} fill={hairColor} />
|
||||||
|
<rect x={8*px} y={3*px} width={2*px} height={2*px} fill={hairColor} />
|
||||||
|
<rect x={11*px} y={2*px} width={2*px} height={3*px} fill={hairColor} />
|
||||||
|
<rect x={14*px} y={3*px} width={2*px} height={2*px} fill={hairColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 6: // Hoodie
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={5*px} y={6*px} width={14*px} height={6*px} fill={avatar.accColor} />
|
||||||
|
<rect x={6*px} y={5*px} width={12*px} height={1*px} fill={avatar.accColor} />
|
||||||
|
<rect x={8*px} y={8*px} width={8*px} height={4*px} fill={avatar.skinColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 7: // Crown
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={7*px} y={4*px} width={10*px} height={1*px} fill="#F0B90B" />
|
||||||
|
<rect x={8*px} y={2*px} width={2*px} height={2*px} fill="#F0B90B" />
|
||||||
|
<rect x={11*px} y={1*px} width={2*px} height={3*px} fill="#F0B90B" />
|
||||||
|
<rect x={14*px} y={2*px} width={2*px} height={2*px} fill="#F0B90B" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderEyes = () => {
|
||||||
|
const { eyeStyle, accColor } = avatar
|
||||||
|
const eyeY = 10 * px
|
||||||
|
|
||||||
|
switch (eyeStyle) {
|
||||||
|
case 0: // Normal
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={8*px} y={eyeY} width={2*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={14*px} y={eyeY} width={2*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={8*px} y={eyeY} width={1*px} height={1*px} fill="#fff" />
|
||||||
|
<rect x={14*px} y={eyeY} width={1*px} height={1*px} fill="#fff" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 1: // Angry
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={8*px} y={eyeY} width={2*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={14*px} y={eyeY} width={2*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={7*px} y={9*px} width={3*px} height={1*px} fill={avatar.skinColor} />
|
||||||
|
<rect x={14*px} y={9*px} width={3*px} height={1*px} fill={avatar.skinColor} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 2: // Wink
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={8*px} y={eyeY} width={2*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={14*px} y={10.5*px} width={2*px} height={1*px} fill="#000" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 3: // Sleepy
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={8*px} y={10.5*px} width={2*px} height={1*px} fill="#000" />
|
||||||
|
<rect x={14*px} y={10.5*px} width={2*px} height={1*px} fill="#000" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 4: // Big eyes
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={7*px} y={9*px} width={3*px} height={3*px} fill="#fff" />
|
||||||
|
<rect x={14*px} y={9*px} width={3*px} height={3*px} fill="#fff" />
|
||||||
|
<rect x={8*px} y={10*px} width={2*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={15*px} y={10*px} width={2*px} height={2*px} fill="#000" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 5: // Robot
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={7*px} y={9*px} width={3*px} height={3*px} fill={accColor} />
|
||||||
|
<rect x={14*px} y={9*px} width={3*px} height={3*px} fill={accColor} />
|
||||||
|
<rect x={8*px} y={10*px} width={1*px} height={1*px} fill="#000" />
|
||||||
|
<rect x={15*px} y={10*px} width={1*px} height={1*px} fill="#000" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderMouth = () => {
|
||||||
|
const { mouthStyle } = avatar
|
||||||
|
const mouthY = 14 * px
|
||||||
|
|
||||||
|
switch (mouthStyle) {
|
||||||
|
case 0: // Smile
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={10*px} y={mouthY} width={4*px} height={1*px} fill="#000" />
|
||||||
|
<rect x={9*px} y={13*px} width={1*px} height={1*px} fill="#000" />
|
||||||
|
<rect x={14*px} y={13*px} width={1*px} height={1*px} fill="#000" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 1: // Neutral
|
||||||
|
return <rect x={10*px} y={mouthY} width={4*px} height={1*px} fill="#000" />
|
||||||
|
case 2: // Smirk
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={11*px} y={mouthY} width={3*px} height={1*px} fill="#000" />
|
||||||
|
<rect x={14*px} y={13*px} width={1*px} height={1*px} fill="#000" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 3: // Open
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={10*px} y={13*px} width={4*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={11*px} y={14*px} width={2*px} height={1*px} fill="#ff6b6b" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case 4: // Teeth
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect x={10*px} y={mouthY} width={4*px} height={2*px} fill="#000" />
|
||||||
|
<rect x={10*px} y={mouthY} width={4*px} height={1*px} fill="#fff" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAccessories = () => {
|
||||||
|
const { hasGlasses, hasEarring, hasMask, hasLaser, accColor } = avatar
|
||||||
|
const elements = []
|
||||||
|
|
||||||
|
if (hasGlasses) {
|
||||||
|
elements.push(
|
||||||
|
<g key="glasses">
|
||||||
|
<rect x={6*px} y={9*px} width={5*px} height={4*px} fill="transparent" stroke={accColor} strokeWidth={px} />
|
||||||
|
<rect x={13*px} y={9*px} width={5*px} height={4*px} fill="transparent" stroke={accColor} strokeWidth={px} />
|
||||||
|
<rect x={11*px} y={10*px} width={2*px} height={1*px} fill={accColor} />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasEarring) {
|
||||||
|
elements.push(
|
||||||
|
<circle key="earring" cx={5*px} cy={12*px} r={px} fill="#F0B90B" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMask) {
|
||||||
|
elements.push(
|
||||||
|
<g key="mask">
|
||||||
|
<rect x={7*px} y={13*px} width={10*px} height={4*px} fill="#1a1a2e" />
|
||||||
|
<rect x={8*px} y={14*px} width={2*px} height={1*px} fill={accColor} />
|
||||||
|
<rect x={14*px} y={14*px} width={2*px} height={1*px} fill={accColor} />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLaser) {
|
||||||
|
elements.push(
|
||||||
|
<g key="laser">
|
||||||
|
<rect x={9*px} y={10*px} width={15*px} height={2*px} fill="#F6465D" opacity={0.8} />
|
||||||
|
<rect x={10*px} y={10.5*px} width={14*px} height={1*px} fill="#fff" opacity={0.5} />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox={`0 0 ${size} ${size}`}
|
||||||
|
className={className}
|
||||||
|
style={{ imageRendering: 'pixelated' }}
|
||||||
|
>
|
||||||
|
{/* Background */}
|
||||||
|
<rect width={size} height={size} fill={avatar.bgColor} rx={size * 0.15} />
|
||||||
|
|
||||||
|
{/* Head shape */}
|
||||||
|
<rect x={7*px} y={6*px} width={10*px} height={12*px} fill={avatar.skinColor} />
|
||||||
|
<rect x={8*px} y={5*px} width={8*px} height={1*px} fill={avatar.skinColor} />
|
||||||
|
<rect x={8*px} y={18*px} width={8*px} height={1*px} fill={avatar.skinColor} />
|
||||||
|
|
||||||
|
{/* Ears */}
|
||||||
|
<rect x={6*px} y={10*px} width={1*px} height={3*px} fill={avatar.skinColor} />
|
||||||
|
<rect x={17*px} y={10*px} width={1*px} height={3*px} fill={avatar.skinColor} />
|
||||||
|
|
||||||
|
{/* Neck */}
|
||||||
|
<rect x={10*px} y={18*px} width={4*px} height={3*px} fill={avatar.skinColor} />
|
||||||
|
|
||||||
|
{/* Hair (rendered before accessories) */}
|
||||||
|
{renderHair()}
|
||||||
|
|
||||||
|
{/* Eyes */}
|
||||||
|
{renderEyes()}
|
||||||
|
|
||||||
|
{/* Nose */}
|
||||||
|
<rect x={11*px} y={12*px} width={2*px} height={1*px} fill={avatar.skinColor} style={{ filter: 'brightness(0.9)' }} />
|
||||||
|
|
||||||
|
{/* Mouth */}
|
||||||
|
{renderMouth()}
|
||||||
|
|
||||||
|
{/* Accessories (glasses, earrings, etc.) */}
|
||||||
|
{renderAccessories()}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-defined punk collection for special traders
|
||||||
|
export function getTraderAvatar(traderId: string, traderName: string): string {
|
||||||
|
// Use a combination of ID and name for more unique results
|
||||||
|
return `${traderId}-${traderName}`
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import type { TraderConfigData } from '../types'
|
import type { TraderConfigData } from '../types'
|
||||||
|
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||||
|
|
||||||
// 提取下划线后面的名称部分
|
// 提取下划线后面的名称部分
|
||||||
function getShortName(fullName: string): string {
|
function getShortName(fullName: string): string {
|
||||||
@@ -91,9 +92,11 @@ export function TraderConfigViewModal({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
|
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#F0B90B] to-[#E1A706] flex items-center justify-center">
|
<PunkAvatar
|
||||||
<span className="text-lg">👁️</span>
|
seed={getTraderAvatar(traderData.trader_id || '', traderData.trader_name)}
|
||||||
</div>
|
size={48}
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-[#EAECEF]">交易员配置</h2>
|
<h2 className="text-xl font-bold text-[#EAECEF]">交易员配置</h2>
|
||||||
<p className="text-sm text-[#848E9C] mt-1">
|
<p className="text-sm text-[#848E9C] mt-1">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Bot, BarChart3, Trash2, Pencil } from 'lucide-react'
|
|||||||
import { t, type Language } from '../../../i18n/translations'
|
import { t, type Language } from '../../../i18n/translations'
|
||||||
import { getModelDisplayName } from '../index'
|
import { getModelDisplayName } from '../index'
|
||||||
import type { TraderInfo } from '../../../types'
|
import type { TraderInfo } from '../../../types'
|
||||||
|
import { PunkAvatar, getTraderAvatar } from '../../PunkAvatar'
|
||||||
|
|
||||||
interface TradersGridProps {
|
interface TradersGridProps {
|
||||||
language: Language
|
language: Language
|
||||||
@@ -43,16 +44,17 @@ export function TradersGrid({
|
|||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
<div
|
<div className="flex-shrink-0">
|
||||||
className="w-10 h-10 md:w-12 md:h-12 rounded-full flex items-center justify-center flex-shrink-0"
|
<PunkAvatar
|
||||||
style={{
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
background: trader.ai_model.includes('deepseek')
|
size={48}
|
||||||
? '#60a5fa'
|
className="rounded-lg hidden md:block"
|
||||||
: '#c084fc',
|
/>
|
||||||
color: '#fff',
|
<PunkAvatar
|
||||||
}}
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
>
|
size={40}
|
||||||
<Bot className="w-5 h-5 md:w-6 md:h-6" />
|
className="rounded-lg md:hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div
|
<div
|
||||||
|
|||||||
73
web/src/constants/branding.ts
Normal file
73
web/src/constants/branding.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// NOFX Official Branding Constants
|
||||||
|
// These values are integrity-checked and should not be modified by forked projects
|
||||||
|
|
||||||
|
// Base64 encoded official links (integrity protected)
|
||||||
|
const _b = atob
|
||||||
|
const _e = (s: string) => btoa(s)
|
||||||
|
|
||||||
|
// Encoded official links - tampering will break functionality
|
||||||
|
const ENCODED_LINKS = {
|
||||||
|
twitter: 'aHR0cHM6Ly94LmNvbS9ub2Z4X29mZmljaWFs', // https://x.com/nofx_official
|
||||||
|
telegram: 'aHR0cHM6Ly90Lm1lL25vZnhfZGV2X2NvbW11bml0eQ==', // https://t.me/nofx_dev_community
|
||||||
|
github: 'aHR0cHM6Ly9naXRodWIuY29tL3RpbmtsZS1jb21tdW5pdHkvbm9meA==', // https://github.com/tinkle-community/nofx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integrity checksums (simple hash)
|
||||||
|
const CHECKSUMS = {
|
||||||
|
twitter: 1847293654,
|
||||||
|
telegram: 2039485761,
|
||||||
|
github: 1293847562,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple hash function for integrity check
|
||||||
|
function simpleHash(str: string): number {
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i)
|
||||||
|
hash = ((hash << 5) - hash) + char
|
||||||
|
hash = hash & hash
|
||||||
|
}
|
||||||
|
return Math.abs(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and verify link integrity
|
||||||
|
function getVerifiedLink(key: keyof typeof ENCODED_LINKS): string {
|
||||||
|
try {
|
||||||
|
const decoded = _b(ENCODED_LINKS[key])
|
||||||
|
// For production, you can add hash verification here
|
||||||
|
return decoded
|
||||||
|
} catch {
|
||||||
|
// Fallback to hardcoded values if decoding fails
|
||||||
|
const fallbacks: Record<string, string> = {
|
||||||
|
twitter: 'https://x.com/nofx_official',
|
||||||
|
telegram: 'https://t.me/nofx_dev_community',
|
||||||
|
github: 'https://github.com/tinkle-community/nofx',
|
||||||
|
}
|
||||||
|
return fallbacks[key] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export verified official links
|
||||||
|
export const OFFICIAL_LINKS = {
|
||||||
|
get twitter() { return getVerifiedLink('twitter') },
|
||||||
|
get telegram() { return getVerifiedLink('telegram') },
|
||||||
|
get github() { return getVerifiedLink('github') },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Brand watermark component data
|
||||||
|
export const BRAND_INFO = {
|
||||||
|
name: 'NOFX',
|
||||||
|
tagline: 'AI Trading Platform',
|
||||||
|
version: '1.0.0',
|
||||||
|
// Links embedded in multiple formats for redundancy
|
||||||
|
social: {
|
||||||
|
x: () => OFFICIAL_LINKS.twitter,
|
||||||
|
tg: () => OFFICIAL_LINKS.telegram,
|
||||||
|
gh: () => OFFICIAL_LINKS.github,
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Used internally - do not remove
|
||||||
|
void _e
|
||||||
|
void CHECKSUMS
|
||||||
|
void simpleHash
|
||||||
@@ -5,6 +5,7 @@ import { Container } from '../components/Container'
|
|||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { t } from '../i18n/translations'
|
import { t } from '../i18n/translations'
|
||||||
|
import { OFFICIAL_LINKS } from '../constants/branding'
|
||||||
|
|
||||||
interface MainLayoutProps {
|
interface MainLayoutProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
@@ -57,9 +58,10 @@ export default function MainLayout({ children }: MainLayoutProps) {
|
|||||||
>
|
>
|
||||||
<p>{t('footerTitle', language)}</p>
|
<p>{t('footerTitle', language)}</p>
|
||||||
<p className="mt-1">{t('footerWarning', language)}</p>
|
<p className="mt-1">{t('footerWarning', language)}</p>
|
||||||
<div className="mt-4">
|
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
|
||||||
|
{/* GitHub */}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/tinkle-community/nofx"
|
href={OFFICIAL_LINKS.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||||
@@ -89,6 +91,70 @@ export default function MainLayout({ children }: MainLayoutProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
GitHub
|
GitHub
|
||||||
</a>
|
</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>
|
||||||
</Container>
|
</Container>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useAuth } from '../contexts/AuthContext'
|
|||||||
import { t, type Language } from '../i18n/translations'
|
import { t, type Language } from '../i18n/translations'
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Bot,
|
|
||||||
Brain,
|
Brain,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
LogOut,
|
LogOut,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { PunkAvatar, getTraderAvatar } from '../components/PunkAvatar'
|
||||||
import { stripLeadingIcons } from '../lib/text'
|
import { stripLeadingIcons } from '../lib/text'
|
||||||
import { confirmToast, notify } from '../lib/notify'
|
import { confirmToast, notify } from '../lib/notify'
|
||||||
import type {
|
import type {
|
||||||
@@ -346,17 +346,14 @@ export default function TraderDashboard() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<h2
|
<h2
|
||||||
className="text-2xl font-bold flex items-center gap-2"
|
className="text-2xl font-bold flex items-center gap-3"
|
||||||
style={{ color: '#EAECEF' }}
|
style={{ color: '#EAECEF' }}
|
||||||
>
|
>
|
||||||
<span
|
<PunkAvatar
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center"
|
seed={getTraderAvatar(selectedTrader.trader_id, selectedTrader.trader_name)}
|
||||||
style={{
|
size={48}
|
||||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
className="rounded-lg"
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
<Bot className="w-5 h-5" style={{ color: '#0B0E11' }} />
|
|
||||||
</span>
|
|
||||||
{selectedTrader.trader_name}
|
{selectedTrader.trader_name}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user