@octo-cyber/sea-king 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/character.controller.d.ts +10 -0
- package/dist/controllers/character.controller.d.ts.map +1 -0
- package/dist/controllers/character.controller.js +58 -0
- package/dist/controllers/character.controller.js.map +1 -0
- package/dist/controllers/chat.controller.d.ts +13 -0
- package/dist/controllers/chat.controller.d.ts.map +1 -0
- package/dist/controllers/chat.controller.js +113 -0
- package/dist/controllers/chat.controller.js.map +1 -0
- package/dist/controllers/crisis.controller.d.ts +10 -0
- package/dist/controllers/crisis.controller.d.ts.map +1 -0
- package/dist/controllers/crisis.controller.js +69 -0
- package/dist/controllers/crisis.controller.js.map +1 -0
- package/dist/controllers/index.d.ts +10 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +24 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/leaderboard.controller.d.ts +9 -0
- package/dist/controllers/leaderboard.controller.d.ts.map +1 -0
- package/dist/controllers/leaderboard.controller.js +51 -0
- package/dist/controllers/leaderboard.controller.js.map +1 -0
- package/dist/controllers/live-chat.controller.d.ts +14 -0
- package/dist/controllers/live-chat.controller.d.ts.map +1 -0
- package/dist/controllers/live-chat.controller.js +95 -0
- package/dist/controllers/live-chat.controller.js.map +1 -0
- package/dist/controllers/matchmaking.controller.d.ts +22 -0
- package/dist/controllers/matchmaking.controller.d.ts.map +1 -0
- package/dist/controllers/matchmaking.controller.js +157 -0
- package/dist/controllers/matchmaking.controller.js.map +1 -0
- package/dist/controllers/phrase.controller.d.ts +11 -0
- package/dist/controllers/phrase.controller.d.ts.map +1 -0
- package/dist/controllers/phrase.controller.js +78 -0
- package/dist/controllers/phrase.controller.js.map +1 -0
- package/dist/controllers/profile.controller.d.ts +9 -0
- package/dist/controllers/profile.controller.d.ts.map +1 -0
- package/dist/controllers/profile.controller.js +44 -0
- package/dist/controllers/profile.controller.js.map +1 -0
- package/dist/controllers/turing.controller.d.ts +18 -0
- package/dist/controllers/turing.controller.d.ts.map +1 -0
- package/dist/controllers/turing.controller.js +103 -0
- package/dist/controllers/turing.controller.js.map +1 -0
- package/dist/entities/chat-character.entity.d.ts +34 -0
- package/dist/entities/chat-character.entity.d.ts.map +1 -0
- package/dist/entities/chat-character.entity.js +101 -0
- package/dist/entities/chat-character.entity.js.map +1 -0
- package/dist/entities/chat-message.entity.d.ts +26 -0
- package/dist/entities/chat-message.entity.d.ts.map +1 -0
- package/dist/entities/chat-message.entity.js +84 -0
- package/dist/entities/chat-message.entity.js.map +1 -0
- package/dist/entities/chat-session.entity.d.ts +31 -0
- package/dist/entities/chat-session.entity.d.ts.map +1 -0
- package/dist/entities/chat-session.entity.js +95 -0
- package/dist/entities/chat-session.entity.js.map +1 -0
- package/dist/entities/chat-thread.entity.d.ts +30 -0
- package/dist/entities/chat-thread.entity.d.ts.map +1 -0
- package/dist/entities/chat-thread.entity.js +105 -0
- package/dist/entities/chat-thread.entity.js.map +1 -0
- package/dist/entities/crisis-event.entity.d.ts +30 -0
- package/dist/entities/crisis-event.entity.d.ts.map +1 -0
- package/dist/entities/crisis-event.entity.js +90 -0
- package/dist/entities/crisis-event.entity.js.map +1 -0
- package/dist/entities/index.d.ts +38 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +62 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/leaderboard-entry.entity.d.ts +22 -0
- package/dist/entities/leaderboard-entry.entity.d.ts.map +1 -0
- package/dist/entities/leaderboard-entry.entity.js +75 -0
- package/dist/entities/leaderboard-entry.entity.js.map +1 -0
- package/dist/entities/live-message.entity.d.ts +34 -0
- package/dist/entities/live-message.entity.d.ts.map +1 -0
- package/dist/entities/live-message.entity.js +99 -0
- package/dist/entities/live-message.entity.js.map +1 -0
- package/dist/entities/live-thread.entity.d.ts +50 -0
- package/dist/entities/live-thread.entity.d.ts.map +1 -0
- package/dist/entities/live-thread.entity.js +144 -0
- package/dist/entities/live-thread.entity.js.map +1 -0
- package/dist/entities/match-room.entity.d.ts +44 -0
- package/dist/entities/match-room.entity.d.ts.map +1 -0
- package/dist/entities/match-room.entity.js +131 -0
- package/dist/entities/match-room.entity.js.map +1 -0
- package/dist/entities/phrase-template.entity.d.ts +28 -0
- package/dist/entities/phrase-template.entity.d.ts.map +1 -0
- package/dist/entities/phrase-template.entity.js +84 -0
- package/dist/entities/phrase-template.entity.js.map +1 -0
- package/dist/entities/player-profile.entity.d.ts +36 -0
- package/dist/entities/player-profile.entity.d.ts.map +1 -0
- package/dist/entities/player-profile.entity.js +112 -0
- package/dist/entities/player-profile.entity.js.map +1 -0
- package/dist/entities/player-queue.entity.d.ts +24 -0
- package/dist/entities/player-queue.entity.d.ts.map +1 -0
- package/dist/entities/player-queue.entity.js +76 -0
- package/dist/entities/player-queue.entity.js.map +1 -0
- package/dist/entities/turing-guess.entity.d.ts +29 -0
- package/dist/entities/turing-guess.entity.d.ts.map +1 -0
- package/dist/entities/turing-guess.entity.js +94 -0
- package/dist/entities/turing-guess.entity.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/sea-king.schema.d.ts +109 -0
- package/dist/schemas/sea-king.schema.d.ts.map +1 -0
- package/dist/schemas/sea-king.schema.js +103 -0
- package/dist/schemas/sea-king.schema.js.map +1 -0
- package/dist/sea-king.module.d.ts +19 -0
- package/dist/sea-king.module.d.ts.map +1 -0
- package/dist/sea-king.module.js +114 -0
- package/dist/sea-king.module.js.map +1 -0
- package/dist/services/character.service.d.ts +30 -0
- package/dist/services/character.service.d.ts.map +1 -0
- package/dist/services/character.service.js +75 -0
- package/dist/services/character.service.js.map +1 -0
- package/dist/services/chat.service.d.ts +49 -0
- package/dist/services/chat.service.d.ts.map +1 -0
- package/dist/services/chat.service.js +332 -0
- package/dist/services/chat.service.js.map +1 -0
- package/dist/services/crisis.service.d.ts +17 -0
- package/dist/services/crisis.service.d.ts.map +1 -0
- package/dist/services/crisis.service.js +177 -0
- package/dist/services/crisis.service.js.map +1 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +28 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/leaderboard.service.d.ts +29 -0
- package/dist/services/leaderboard.service.d.ts.map +1 -0
- package/dist/services/leaderboard.service.js +64 -0
- package/dist/services/leaderboard.service.js.map +1 -0
- package/dist/services/live-chat.service.d.ts +71 -0
- package/dist/services/live-chat.service.d.ts.map +1 -0
- package/dist/services/live-chat.service.js +273 -0
- package/dist/services/live-chat.service.js.map +1 -0
- package/dist/services/matchmaking.service.d.ts +60 -0
- package/dist/services/matchmaking.service.d.ts.map +1 -0
- package/dist/services/matchmaking.service.js +208 -0
- package/dist/services/matchmaking.service.js.map +1 -0
- package/dist/services/phrase.service.d.ts +27 -0
- package/dist/services/phrase.service.d.ts.map +1 -0
- package/dist/services/phrase.service.js +70 -0
- package/dist/services/phrase.service.js.map +1 -0
- package/dist/services/profile.service.d.ts +41 -0
- package/dist/services/profile.service.d.ts.map +1 -0
- package/dist/services/profile.service.js +107 -0
- package/dist/services/profile.service.js.map +1 -0
- package/dist/services/seed.service.d.ts +19 -0
- package/dist/services/seed.service.d.ts.map +1 -0
- package/dist/services/seed.service.js +550 -0
- package/dist/services/seed.service.js.map +1 -0
- package/dist/services/turing.service.d.ts +74 -0
- package/dist/services/turing.service.d.ts.map +1 -0
- package/dist/services/turing.service.js +175 -0
- package/dist/services/turing.service.js.map +1 -0
- package/dist/services/ws-hub.service.d.ts +51 -0
- package/dist/services/ws-hub.service.d.ts.map +1 -0
- package/dist/services/ws-hub.service.js +174 -0
- package/dist/services/ws-hub.service.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
- package/web/index.ts +30 -0
- package/web/manifest.ts +40 -0
- package/web/messages/en-US.json +113 -0
- package/web/messages/zh-CN.json +113 -0
- package/web/pages/AnalysisPage.tsx +198 -0
- package/web/pages/ChatPage.tsx +482 -0
- package/web/pages/DashboardPage.tsx +281 -0
- package/web/pages/LeaderboardPage.tsx +208 -0
- package/web/pages/PhrasesPage.tsx +221 -0
- package/web/services/sea-king-service.ts +151 -0
- package/web/types/sea-king.ts +249 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
|
+
import {
|
|
6
|
+
Crown,
|
|
7
|
+
MessageCircleHeart,
|
|
8
|
+
Flame,
|
|
9
|
+
Trophy,
|
|
10
|
+
Zap,
|
|
11
|
+
Play,
|
|
12
|
+
TrendingUp,
|
|
13
|
+
Shield,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import { getAnalysis, getChatHistory } from '../services/sea-king-service';
|
|
16
|
+
import type { PlayerAnalysis, ChatSession } from '../types/sea-king';
|
|
17
|
+
|
|
18
|
+
const RANK_EMOJIS: Record<string, string> = {
|
|
19
|
+
bronze: '🥉',
|
|
20
|
+
silver: '🥈',
|
|
21
|
+
gold: '🥇',
|
|
22
|
+
platinum: '💎',
|
|
23
|
+
diamond: '💠',
|
|
24
|
+
master: '👑',
|
|
25
|
+
king: '🔱',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default function DashboardPage() {
|
|
29
|
+
const t = useTranslations('seaKing.dashboard');
|
|
30
|
+
const tRanks = useTranslations('seaKing.ranks');
|
|
31
|
+
|
|
32
|
+
const [analysis, setAnalysis] = useState<PlayerAnalysis | null>(null);
|
|
33
|
+
const [recentSessions, setRecentSessions] = useState<ChatSession[]>([]);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
Promise.allSettled([getAnalysis(), getChatHistory(1, 5)]).then(
|
|
38
|
+
([analysisRes, historyRes]) => {
|
|
39
|
+
if (analysisRes.status === 'fulfilled') setAnalysis(analysisRes.value);
|
|
40
|
+
if (historyRes.status === 'fulfilled') setRecentSessions(historyRes.value.items);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
if (loading) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
49
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-cyan-500" />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rank = analysis?.profile.rank ?? 'bronze';
|
|
55
|
+
const rankEmoji = RANK_EMOJIS[rank] ?? '🥉';
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-6 p-6">
|
|
59
|
+
{/* Hero Banner */}
|
|
60
|
+
<div className="bg-gradient-to-r from-cyan-500 via-blue-500 to-purple-600 rounded-2xl p-8 text-white relative overflow-hidden">
|
|
61
|
+
<div className="absolute top-0 right-0 opacity-10 text-[200px] leading-none select-none">
|
|
62
|
+
🌊
|
|
63
|
+
</div>
|
|
64
|
+
<div className="relative z-10">
|
|
65
|
+
<div className="flex items-center gap-3 mb-2">
|
|
66
|
+
<Crown className="h-8 w-8" />
|
|
67
|
+
<h1 className="text-3xl font-bold">{t('welcomeTitle')}</h1>
|
|
68
|
+
</div>
|
|
69
|
+
<p className="text-cyan-100 text-lg mt-2">{t('welcomeSubtitle')}</p>
|
|
70
|
+
<div className="mt-4 flex items-center gap-2">
|
|
71
|
+
<span className="text-4xl">{rankEmoji}</span>
|
|
72
|
+
<div>
|
|
73
|
+
<div className="text-sm opacity-80">{t('currentRank')}</div>
|
|
74
|
+
<div className="text-xl font-bold">{tRanks(rank)}</div>
|
|
75
|
+
</div>
|
|
76
|
+
{analysis?.rankInfo.nextRank && (
|
|
77
|
+
<div className="ml-6 bg-white/20 rounded-lg px-3 py-1 text-sm">
|
|
78
|
+
{t('pointsToNext', { points: analysis.rankInfo.pointsToNext })}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Stats Grid */}
|
|
86
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
87
|
+
<StatCard
|
|
88
|
+
icon={<MessageCircleHeart className="h-5 w-5 text-pink-500" />}
|
|
89
|
+
label={t('totalFavor')}
|
|
90
|
+
value={`${analysis?.profile.totalFavorGained ?? 0}`}
|
|
91
|
+
bg="bg-pink-50"
|
|
92
|
+
border="border-pink-200"
|
|
93
|
+
/>
|
|
94
|
+
<StatCard
|
|
95
|
+
icon={<Flame className="h-5 w-5 text-orange-500" />}
|
|
96
|
+
label={t('totalSessions')}
|
|
97
|
+
value={`${analysis?.profile.totalSessions ?? 0}`}
|
|
98
|
+
bg="bg-orange-50"
|
|
99
|
+
border="border-orange-200"
|
|
100
|
+
/>
|
|
101
|
+
<StatCard
|
|
102
|
+
icon={<Shield className="h-5 w-5 text-green-500" />}
|
|
103
|
+
label={t('crisisResolved')}
|
|
104
|
+
value={`${analysis?.profile.crisisResolved ?? 0}`}
|
|
105
|
+
bg="bg-green-50"
|
|
106
|
+
border="border-green-200"
|
|
107
|
+
/>
|
|
108
|
+
<StatCard
|
|
109
|
+
icon={<TrendingUp className="h-5 w-5 text-blue-500" />}
|
|
110
|
+
label={t('bestStreak')}
|
|
111
|
+
value={`${analysis?.profile.bestStreak ?? 0}`}
|
|
112
|
+
bg="bg-blue-50"
|
|
113
|
+
border="border-blue-200"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Skill Radar Preview */}
|
|
118
|
+
{analysis && (
|
|
119
|
+
<div className="bg-white rounded-xl border p-6">
|
|
120
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
121
|
+
<Zap className="h-5 w-5 text-amber-500" />
|
|
122
|
+
{t('skillRadar')}
|
|
123
|
+
</h2>
|
|
124
|
+
<div className="grid grid-cols-5 gap-4">
|
|
125
|
+
{analysis.radarData.map((d) => (
|
|
126
|
+
<div key={d.dimension} className="text-center">
|
|
127
|
+
<div className="relative w-16 h-16 mx-auto mb-2">
|
|
128
|
+
<svg viewBox="0 0 36 36" className="w-16 h-16 transform -rotate-90">
|
|
129
|
+
<path
|
|
130
|
+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
131
|
+
fill="none"
|
|
132
|
+
stroke="#e5e7eb"
|
|
133
|
+
strokeWidth="3"
|
|
134
|
+
/>
|
|
135
|
+
<path
|
|
136
|
+
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
137
|
+
fill="none"
|
|
138
|
+
stroke="url(#gradient)"
|
|
139
|
+
strokeWidth="3"
|
|
140
|
+
strokeDasharray={`${d.score}, 100`}
|
|
141
|
+
strokeLinecap="round"
|
|
142
|
+
/>
|
|
143
|
+
<defs>
|
|
144
|
+
<linearGradient id="gradient">
|
|
145
|
+
<stop offset="0%" stopColor="#06b6d4" />
|
|
146
|
+
<stop offset="100%" stopColor="#8b5cf6" />
|
|
147
|
+
</linearGradient>
|
|
148
|
+
</defs>
|
|
149
|
+
</svg>
|
|
150
|
+
<span className="absolute inset-0 flex items-center justify-center text-sm font-bold">
|
|
151
|
+
{d.score}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="text-xs text-gray-600">{d.label}</div>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Quick Start */}
|
|
162
|
+
<div>
|
|
163
|
+
<h2 className="text-lg font-semibold mb-4">{t('quickStart')}</h2>
|
|
164
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
165
|
+
<QuickStartCard
|
|
166
|
+
href="/sea-king/chat"
|
|
167
|
+
emoji="💬"
|
|
168
|
+
title={t('startChat')}
|
|
169
|
+
description={t('startChatDesc')}
|
|
170
|
+
color="from-cyan-500 to-blue-500"
|
|
171
|
+
/>
|
|
172
|
+
<QuickStartCard
|
|
173
|
+
href="/sea-king/phrases"
|
|
174
|
+
emoji="📚"
|
|
175
|
+
title={t('learnPhrases')}
|
|
176
|
+
description={t('learnPhrasesDesc')}
|
|
177
|
+
color="from-pink-500 to-rose-500"
|
|
178
|
+
/>
|
|
179
|
+
<QuickStartCard
|
|
180
|
+
href="/sea-king/leaderboard"
|
|
181
|
+
emoji="🏆"
|
|
182
|
+
title={t('viewRanking')}
|
|
183
|
+
description={t('viewRankingDesc')}
|
|
184
|
+
color="from-amber-500 to-orange-500"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Recent Sessions */}
|
|
190
|
+
{recentSessions.length > 0 && (
|
|
191
|
+
<div className="bg-white rounded-xl border p-6">
|
|
192
|
+
<h2 className="text-lg font-semibold mb-4">{t('recentSessions')}</h2>
|
|
193
|
+
<div className="space-y-3">
|
|
194
|
+
{recentSessions.map((session) => (
|
|
195
|
+
<div
|
|
196
|
+
key={session.id}
|
|
197
|
+
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
|
198
|
+
>
|
|
199
|
+
<div className="flex items-center gap-3">
|
|
200
|
+
<span className="text-2xl">
|
|
201
|
+
{session.status === 'completed' ? '✅' : session.status === 'failed' ? '💔' : '⏳'}
|
|
202
|
+
</span>
|
|
203
|
+
<div>
|
|
204
|
+
<div className="font-medium">
|
|
205
|
+
{session.characterCount} {t('characters')} · {t('score')}: {session.totalFavorScore}
|
|
206
|
+
</div>
|
|
207
|
+
<div className="text-sm text-gray-500">
|
|
208
|
+
{new Date(session.createdAt).toLocaleDateString()}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="text-sm text-gray-400">
|
|
213
|
+
{RANK_EMOJIS[session.rank] ?? '🥉'} {tRanks(session.rank)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function StatCard({
|
|
225
|
+
icon,
|
|
226
|
+
label,
|
|
227
|
+
value,
|
|
228
|
+
bg,
|
|
229
|
+
border,
|
|
230
|
+
}: {
|
|
231
|
+
icon: React.ReactNode;
|
|
232
|
+
label: string;
|
|
233
|
+
value: string;
|
|
234
|
+
bg: string;
|
|
235
|
+
border: string;
|
|
236
|
+
}) {
|
|
237
|
+
return (
|
|
238
|
+
<div className={`${bg} ${border} border rounded-xl p-4`}>
|
|
239
|
+
<div className="flex items-center gap-2 mb-2">
|
|
240
|
+
{icon}
|
|
241
|
+
<span className="text-sm text-gray-600">{label}</span>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="text-2xl font-bold">{value}</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function QuickStartCard({
|
|
249
|
+
href,
|
|
250
|
+
emoji,
|
|
251
|
+
title,
|
|
252
|
+
description,
|
|
253
|
+
color,
|
|
254
|
+
}: {
|
|
255
|
+
href: string;
|
|
256
|
+
emoji: string;
|
|
257
|
+
title: string;
|
|
258
|
+
description: string;
|
|
259
|
+
color: string;
|
|
260
|
+
}) {
|
|
261
|
+
return (
|
|
262
|
+
<a
|
|
263
|
+
href={href}
|
|
264
|
+
className="group block rounded-xl border p-5 hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
|
|
265
|
+
>
|
|
266
|
+
<div className="flex items-center gap-3">
|
|
267
|
+
<div
|
|
268
|
+
className={`bg-gradient-to-br ${color} rounded-xl p-3 text-white shadow-lg`}
|
|
269
|
+
>
|
|
270
|
+
<span className="text-xl">{emoji}</span>
|
|
271
|
+
</div>
|
|
272
|
+
<div>
|
|
273
|
+
<h3 className="font-semibold group-hover:text-cyan-600 transition-colors">
|
|
274
|
+
{title}
|
|
275
|
+
</h3>
|
|
276
|
+
<p className="text-sm text-gray-500">{description}</p>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</a>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
|
+
import { Trophy, Crown, Medal, Star, Users, Flame, Shield } from 'lucide-react';
|
|
6
|
+
import { getLeaderboard } from '../services/sea-king-service';
|
|
7
|
+
import type { LeaderboardEntry } from '../types/sea-king';
|
|
8
|
+
|
|
9
|
+
const RANK_EMOJIS: Record<string, string> = {
|
|
10
|
+
bronze: '🥉',
|
|
11
|
+
silver: '🥈',
|
|
12
|
+
gold: '🥇',
|
|
13
|
+
platinum: '💎',
|
|
14
|
+
diamond: '💠',
|
|
15
|
+
master: '👑',
|
|
16
|
+
king: '🔱',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const TOP_3_STYLES = [
|
|
20
|
+
'bg-gradient-to-r from-amber-400 to-yellow-500 text-white', // 1st
|
|
21
|
+
'bg-gradient-to-r from-gray-300 to-gray-400 text-white', // 2nd
|
|
22
|
+
'bg-gradient-to-r from-orange-400 to-amber-500 text-white', // 3rd
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default function LeaderboardPage() {
|
|
26
|
+
const t = useTranslations('seaKing.leaderboard');
|
|
27
|
+
const tRanks = useTranslations('seaKing.ranks');
|
|
28
|
+
|
|
29
|
+
const [entries, setEntries] = useState<LeaderboardEntry[]>([]);
|
|
30
|
+
const [total, setTotal] = useState(0);
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [page, setPage] = useState(1);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setLoading(true);
|
|
36
|
+
getLeaderboard({ page, pageSize: 20 })
|
|
37
|
+
.then((data) => {
|
|
38
|
+
setEntries(data.items);
|
|
39
|
+
setTotal(data.total);
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {})
|
|
42
|
+
.finally(() => setLoading(false));
|
|
43
|
+
}, [page]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-6 p-6">
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<div className="bg-gradient-to-r from-amber-500 via-yellow-500 to-orange-500 rounded-2xl p-8 text-white relative overflow-hidden">
|
|
49
|
+
<div className="absolute top-0 right-0 opacity-10 text-[180px] leading-none select-none">
|
|
50
|
+
🏆
|
|
51
|
+
</div>
|
|
52
|
+
<div className="relative z-10">
|
|
53
|
+
<div className="flex items-center gap-3 mb-2">
|
|
54
|
+
<Trophy className="h-8 w-8" />
|
|
55
|
+
<h1 className="text-3xl font-bold">{t('title')}</h1>
|
|
56
|
+
</div>
|
|
57
|
+
<p className="text-amber-100 text-lg mt-2">{t('subtitle')}</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Top 3 Podium */}
|
|
62
|
+
{entries.length >= 3 && (
|
|
63
|
+
<div className="grid grid-cols-3 gap-4">
|
|
64
|
+
{/* 2nd Place */}
|
|
65
|
+
<div className="text-center mt-8">
|
|
66
|
+
<div className="bg-white rounded-xl border shadow-lg p-4">
|
|
67
|
+
<div className="text-4xl mb-2">🥈</div>
|
|
68
|
+
<div className="font-bold text-lg">{entries[1]?.displayName}</div>
|
|
69
|
+
<div className="text-2xl font-bold text-gray-500">{entries[1]?.totalScore}</div>
|
|
70
|
+
<div className="text-xs text-gray-400 mt-1">
|
|
71
|
+
{RANK_EMOJIS[entries[1]?.rank ?? 'bronze']} {tRanks(entries[1]?.rank ?? 'bronze')}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* 1st Place */}
|
|
77
|
+
<div className="text-center">
|
|
78
|
+
<div className="bg-gradient-to-b from-amber-50 to-yellow-50 rounded-xl border-2 border-amber-300 shadow-xl p-4">
|
|
79
|
+
<div className="text-5xl mb-2 animate-bounce">👑</div>
|
|
80
|
+
<div className="font-bold text-xl">{entries[0]?.displayName}</div>
|
|
81
|
+
<div className="text-3xl font-bold text-amber-600">{entries[0]?.totalScore}</div>
|
|
82
|
+
<div className="text-xs text-amber-500 mt-1 font-medium">
|
|
83
|
+
{RANK_EMOJIS[entries[0]?.rank ?? 'bronze']} {tRanks(entries[0]?.rank ?? 'bronze')}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* 3rd Place */}
|
|
89
|
+
<div className="text-center mt-12">
|
|
90
|
+
<div className="bg-white rounded-xl border shadow-lg p-4">
|
|
91
|
+
<div className="text-3xl mb-2">🥉</div>
|
|
92
|
+
<div className="font-bold">{entries[2]?.displayName}</div>
|
|
93
|
+
<div className="text-xl font-bold text-orange-500">{entries[2]?.totalScore}</div>
|
|
94
|
+
<div className="text-xs text-gray-400 mt-1">
|
|
95
|
+
{RANK_EMOJIS[entries[2]?.rank ?? 'bronze']} {tRanks(entries[2]?.rank ?? 'bronze')}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{/* Full Ranking Table */}
|
|
103
|
+
<div className="bg-white rounded-xl border overflow-hidden">
|
|
104
|
+
<table className="w-full">
|
|
105
|
+
<thead className="bg-gray-50 border-b">
|
|
106
|
+
<tr>
|
|
107
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
108
|
+
{t('rankCol')}
|
|
109
|
+
</th>
|
|
110
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
111
|
+
{t('player')}
|
|
112
|
+
</th>
|
|
113
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
114
|
+
{t('tier')}
|
|
115
|
+
</th>
|
|
116
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
|
117
|
+
{t('score')}
|
|
118
|
+
</th>
|
|
119
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
|
120
|
+
{t('maxChats')}
|
|
121
|
+
</th>
|
|
122
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
|
123
|
+
{t('crisisRate')}
|
|
124
|
+
</th>
|
|
125
|
+
</tr>
|
|
126
|
+
</thead>
|
|
127
|
+
<tbody className="divide-y divide-gray-100">
|
|
128
|
+
{loading ? (
|
|
129
|
+
<tr>
|
|
130
|
+
<td colSpan={6} className="px-6 py-8 text-center">
|
|
131
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-amber-500 mx-auto" />
|
|
132
|
+
</td>
|
|
133
|
+
</tr>
|
|
134
|
+
) : entries.length === 0 ? (
|
|
135
|
+
<tr>
|
|
136
|
+
<td colSpan={6} className="px-6 py-12 text-center text-gray-400">
|
|
137
|
+
<span className="text-4xl">🏜️</span>
|
|
138
|
+
<p className="mt-2">{t('empty')}</p>
|
|
139
|
+
</td>
|
|
140
|
+
</tr>
|
|
141
|
+
) : (
|
|
142
|
+
entries.map((entry, idx) => {
|
|
143
|
+
const position = (page - 1) * 20 + idx + 1;
|
|
144
|
+
return (
|
|
145
|
+
<tr key={entry.id} className="hover:bg-gray-50 transition-colors">
|
|
146
|
+
<td className="px-6 py-4">
|
|
147
|
+
{position <= 3 ? (
|
|
148
|
+
<span className="text-xl">
|
|
149
|
+
{position === 1 ? '🥇' : position === 2 ? '🥈' : '🥉'}
|
|
150
|
+
</span>
|
|
151
|
+
) : (
|
|
152
|
+
<span className="text-gray-400 font-mono">#{position}</span>
|
|
153
|
+
)}
|
|
154
|
+
</td>
|
|
155
|
+
<td className="px-6 py-4 font-medium">{entry.displayName}</td>
|
|
156
|
+
<td className="px-6 py-4">
|
|
157
|
+
<span className="text-sm">
|
|
158
|
+
{RANK_EMOJIS[entry.rank] ?? '🥉'} {tRanks(entry.rank)}
|
|
159
|
+
</span>
|
|
160
|
+
</td>
|
|
161
|
+
<td className="px-6 py-4 text-right font-bold text-amber-600">
|
|
162
|
+
{entry.totalScore}
|
|
163
|
+
</td>
|
|
164
|
+
<td className="px-6 py-4 text-right">
|
|
165
|
+
<span className="flex items-center justify-end gap-1 text-sm">
|
|
166
|
+
<Users className="h-3 w-3 text-gray-400" />
|
|
167
|
+
{entry.maxConcurrentChats}
|
|
168
|
+
</span>
|
|
169
|
+
</td>
|
|
170
|
+
<td className="px-6 py-4 text-right">
|
|
171
|
+
<span className="flex items-center justify-end gap-1 text-sm">
|
|
172
|
+
<Shield className="h-3 w-3 text-gray-400" />
|
|
173
|
+
{entry.crisisSuccessRate}%
|
|
174
|
+
</span>
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
);
|
|
178
|
+
})
|
|
179
|
+
)}
|
|
180
|
+
</tbody>
|
|
181
|
+
</table>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Pagination */}
|
|
185
|
+
{total > 20 && (
|
|
186
|
+
<div className="flex justify-center gap-2">
|
|
187
|
+
<button
|
|
188
|
+
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
189
|
+
disabled={page === 1}
|
|
190
|
+
className="px-4 py-2 rounded-lg border hover:bg-gray-50 disabled:opacity-30"
|
|
191
|
+
>
|
|
192
|
+
{t('prev')}
|
|
193
|
+
</button>
|
|
194
|
+
<span className="px-4 py-2 text-gray-500">
|
|
195
|
+
{page} / {Math.ceil(total / 20)}
|
|
196
|
+
</span>
|
|
197
|
+
<button
|
|
198
|
+
onClick={() => setPage((p) => p + 1)}
|
|
199
|
+
disabled={page >= Math.ceil(total / 20)}
|
|
200
|
+
className="px-4 py-2 rounded-lg border hover:bg-gray-50 disabled:opacity-30"
|
|
201
|
+
>
|
|
202
|
+
{t('next')}
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|