You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
5.0 KiB
TypeScript
197 lines
5.0 KiB
TypeScript
import { useState, type CSSProperties } from 'react';
|
|
import type { Achievement } from '../network/api';
|
|
|
|
interface AchievementsPanelProps {
|
|
achievements: Achievement[];
|
|
}
|
|
|
|
const buttonStyle: CSSProperties = {
|
|
position: 'fixed',
|
|
top: 12,
|
|
right: 240,
|
|
zIndex: 50,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
padding: '5px 10px',
|
|
borderRadius: 8,
|
|
border: '1px solid rgba(255, 215, 0, 0.2)',
|
|
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
color: '#daa520',
|
|
fontSize: 11,
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
pointerEvents: 'auto',
|
|
userSelect: 'none',
|
|
};
|
|
|
|
const panelStyle: CSSProperties = {
|
|
position: 'fixed',
|
|
top: 12,
|
|
right: 240,
|
|
zIndex: 50,
|
|
width: 260,
|
|
maxHeight: 340,
|
|
borderRadius: 10,
|
|
border: '1px solid rgba(255, 215, 0, 0.15)',
|
|
backgroundColor: 'rgba(10, 10, 20, 0.88)',
|
|
backdropFilter: 'blur(6px)',
|
|
overflow: 'hidden',
|
|
pointerEvents: 'auto',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
};
|
|
|
|
const panelHeaderStyle: CSSProperties = {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: '7px 10px',
|
|
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
color: '#daa520',
|
|
flexShrink: 0,
|
|
};
|
|
|
|
const closeButtonStyle: CSSProperties = {
|
|
background: 'none',
|
|
border: 'none',
|
|
color: '#888',
|
|
fontSize: 14,
|
|
cursor: 'pointer',
|
|
padding: '0 4px',
|
|
lineHeight: 1,
|
|
};
|
|
|
|
const listStyle: CSSProperties = {
|
|
padding: '6px 10px 8px',
|
|
overflowY: 'auto',
|
|
flex: 1,
|
|
};
|
|
|
|
function achievementRowStyle(unlocked: boolean): CSSProperties {
|
|
return {
|
|
marginBottom: 8,
|
|
padding: '6px 8px',
|
|
borderRadius: 6,
|
|
border: unlocked
|
|
? '1px solid rgba(255, 215, 0, 0.5)'
|
|
: '1px solid rgba(255, 255, 255, 0.08)',
|
|
backgroundColor: unlocked
|
|
? 'rgba(255, 215, 0, 0.06)'
|
|
: 'rgba(255, 255, 255, 0.02)',
|
|
};
|
|
}
|
|
|
|
const titleRowStyle: CSSProperties = {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 3,
|
|
};
|
|
|
|
const progressBarBgStyle: CSSProperties = {
|
|
width: '100%',
|
|
height: 5,
|
|
borderRadius: 3,
|
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
overflow: 'hidden',
|
|
marginTop: 4,
|
|
};
|
|
|
|
function progressBarFillStyle(pct: number, unlocked: boolean): CSSProperties {
|
|
return {
|
|
width: `${Math.min(100, pct)}%`,
|
|
height: '100%',
|
|
borderRadius: 3,
|
|
backgroundColor: unlocked ? '#ffd700' : '#daa520',
|
|
transition: 'width 300ms ease',
|
|
};
|
|
}
|
|
|
|
function formatDate(iso: string): string {
|
|
try {
|
|
const d = new Date(iso);
|
|
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function AchievementsPanel({ achievements }: AchievementsPanelProps) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const unlockedCount = achievements.filter((a) => a.unlocked).length;
|
|
|
|
if (!expanded) {
|
|
return (
|
|
<button style={buttonStyle} onClick={() => setExpanded(true)}>
|
|
<span style={{ fontSize: 13 }}>🏆</span>
|
|
<span>{unlockedCount}/{achievements.length}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={panelStyle}>
|
|
<div style={panelHeaderStyle}>
|
|
<span>🏆 Achievements</span>
|
|
<button style={closeButtonStyle} onClick={() => setExpanded(false)}>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<div style={listStyle}>
|
|
{achievements.map((ach) => {
|
|
const pct = ach.conditionValue > 0
|
|
? (Math.min(ach.conditionValue, ach.conditionValue) / ach.conditionValue) * 100
|
|
: 0;
|
|
|
|
return (
|
|
<div key={ach.id} style={achievementRowStyle(ach.unlocked)}>
|
|
<div style={titleRowStyle}>
|
|
<span style={{
|
|
fontSize: 11,
|
|
fontWeight: 600,
|
|
color: ach.unlocked ? '#ffd700' : '#ccc',
|
|
opacity: ach.unlocked ? 1 : 0.85,
|
|
}}>
|
|
{ach.unlocked ? '\u2714 ' : ''}{ach.title}
|
|
</span>
|
|
{ach.unlocked && ach.unlockedAt && (
|
|
<span style={{ fontSize: 9, color: '#999' }}>
|
|
{formatDate(ach.unlockedAt)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div style={{ fontSize: 10, color: '#aaa', marginBottom: 2 }}>
|
|
{ach.description}
|
|
</div>
|
|
{!ach.unlocked && (
|
|
<div style={progressBarBgStyle}>
|
|
<div style={progressBarFillStyle(pct, false)} />
|
|
</div>
|
|
)}
|
|
{ach.unlocked && (
|
|
<div style={{
|
|
fontSize: 9,
|
|
color: '#b8860b',
|
|
marginTop: 2,
|
|
fontWeight: 600,
|
|
}}>
|
|
+{ach.rewardAmount} {ach.rewardType}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
{achievements.length === 0 && (
|
|
<div style={{ textAlign: 'center', opacity: 0.5, padding: 8, fontSize: 11, color: '#999' }}>
|
|
No achievements yet
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|