Skip to content

Commit ddfa583

Browse files
Merge pull request #118 from kc3hack/issue-116-unify-badge-display
feat: #116 /gradesページのバッジ表示を/dashboardデザインに統一
2 parents 7b80882 + 21b583d commit ddfa583

File tree

3 files changed

+139
-35
lines changed

3 files changed

+139
-35
lines changed

frontend/src/features/grades/api/mock.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,73 @@ export const getGradeStats = async (): Promise<GradeStats> => {
1818
export const getBadges = async (): Promise<Badge[]> => {
1919
// TODO: Replace with actual API call
2020
return [
21-
{ id: '1', name: 'Master', icon: '🏆', type: 'trophy', earnedAt: '2025-01-01' },
22-
{ id: '2', name: 'Gold', icon: '🥇', type: 'gold', earnedAt: '2025-01-02' },
23-
{ id: '3', name: 'Silver', icon: '🥈', type: 'silver', earnedAt: '2025-01-03' },
24-
{ id: '4', name: 'Silver', icon: '🥈', type: 'silver', earnedAt: '2025-01-04' },
25-
{ id: '5', name: 'Bronze', icon: '🥉', type: 'bronze', earnedAt: '2025-01-05' },
26-
{ id: '6', name: 'Bronze', icon: '🥉', type: 'bronze', earnedAt: '2025-01-06' },
27-
{ id: '7', name: 'Bronze', icon: '🥉', type: 'bronze', earnedAt: '2025-01-07' },
21+
{
22+
id: '1',
23+
name: 'Master Trophy',
24+
type: 'trophy',
25+
image: '/images/badges/Trophy.png',
26+
sortOrder: 1,
27+
earnedAt: '2025-01-01'
28+
},
29+
{
30+
id: '5',
31+
name: 'Design Seed',
32+
type: 'rank',
33+
image: '/images/ranks/rank_tree_0.png',
34+
category: 'design',
35+
rankLevel: 0,
36+
sortOrder: 2,
37+
earnedAt: '2025-01-05'
38+
},
39+
{
40+
id: '2',
41+
name: 'AI Sprout',
42+
type: 'rank',
43+
image: '/images/ranks/rank_tree_1.png',
44+
category: 'ai',
45+
rankLevel: 1,
46+
sortOrder: 3,
47+
earnedAt: '2025-01-02'
48+
},
49+
{
50+
id: '3',
51+
name: 'Web Sprout',
52+
type: 'rank',
53+
image: '/images/ranks/rank_tree_1.png',
54+
category: 'web',
55+
rankLevel: 1,
56+
sortOrder: 3,
57+
earnedAt: '2025-01-03'
58+
},
59+
{
60+
id: '4',
61+
name: 'Security Giant Tree',
62+
type: 'rank',
63+
image: '/images/ranks/rank_tree_3.png',
64+
category: 'security',
65+
rankLevel: 3,
66+
sortOrder: 4,
67+
earnedAt: '2025-01-04'
68+
},
69+
{
70+
id: '7',
71+
name: 'Web Giant Tree',
72+
type: 'rank',
73+
image: '/images/ranks/rank_tree_3.png',
74+
category: 'web',
75+
rankLevel: 3,
76+
sortOrder: 4,
77+
earnedAt: '2025-01-07'
78+
},
79+
{
80+
id: '6',
81+
name: 'Infra Grove',
82+
type: 'rank',
83+
image: '/images/ranks/rank_tree_5.png',
84+
category: 'infra',
85+
rankLevel: 5,
86+
sortOrder: 5,
87+
earnedAt: '2025-01-06'
88+
},
2889
];
2990
};

frontend/src/features/grades/components/BadgeList.tsx

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,23 @@ interface BadgeListProps {
88
badges: Badge[];
99
}
1010

11-
// バッジタイプに応じた画像マッピング関数
12-
const getBadgeImage = (badge: Badge) => {
13-
// まずtypeで判別
14-
if (badge.type === 'trophy') return '/images/badges/Trophy.png';
15-
if (badge.type === 'gold') return '/images/badges/AI_basic.png';
16-
if (badge.type === 'silver') return '/images/badges/Web_base.png';
17-
if (badge.type === 'bronze') return '/images/badges/Seed.png';
18-
19-
// IDや名前でも判別(後方互換性)
20-
if (badge.id.includes('web')) return '/images/badges/Web_base.png';
21-
if (badge.id.includes('ai')) return '/images/badges/AI_basic.png';
22-
if (badge.name.includes('Seed')) return '/images/badges/Seed.png';
23-
if (badge.name.includes('Trophy') || badge.id.includes('trophy')) return '/images/badges/Trophy.png';
24-
25-
// デフォルト
26-
return '/images/badges/Trophy.png';
11+
// カテゴリ色定義
12+
const CATEGORY_COLORS: Record<string, string> = {
13+
web: "#55aaff",
14+
ai: "#e8b849",
15+
security: "#e85555",
16+
infra: "#55cc55",
17+
design: "#cc66dd",
2718
};
2819

2920
export const BadgeList: React.FC<BadgeListProps> = ({ badges }) => {
21+
// ソート: トロフィー → 初級 → 中級 → 上級
22+
const sortedBadges = [...badges].sort((a, b) => {
23+
if (a.sortOrder !== b.sortOrder) {
24+
return a.sortOrder - b.sortOrder;
25+
}
26+
return parseInt(a.id) - parseInt(b.id);
27+
});
3028
return (
3129
<div className="mx-auto w-full max-w-5xl px-4">
3230
<div className="mb-4 flex items-center gap-4 pl-2">
@@ -53,8 +51,8 @@ export const BadgeList: React.FC<BadgeListProps> = ({ badges }) => {
5351
{/* バッジ一覧を横スクロール表示 */}
5452
{badges.length > 0 ? (
5553
<div className="flex min-w-max gap-6 items-end pb-4">
56-
{badges.map((badge, index) => {
57-
const isTrophy = badge.name.includes('Trophy') || badge.id.includes('trophy');
54+
{sortedBadges.map((badge, index) => {
55+
const isTrophy = badge.type === 'trophy';
5856
const sizeClass = isTrophy ? 'h-80 w-80' : 'h-60 w-60';
5957
const animationDelay = index * 0.2;
6058

@@ -63,20 +61,62 @@ export const BadgeList: React.FC<BadgeListProps> = ({ badges }) => {
6361
key={badge.id}
6462
className="flex flex-col items-center gap-2 group"
6563
>
66-
<span className="text-xl font-bold text-[#1a4023] opacity-0 group-hover:opacity-100 group-hover:animate-bounce transition-opacity"></span>
64+
<span className="text-xl font-bold text-[#2C5F2D] opacity-0 group-hover:opacity-100 group-hover:animate-bounce transition-opacity">
65+
66+
</span>
6767
<div
68-
className={`relative ${sizeClass} filter drop-shadow-[4px_4px_0_rgba(26,64,35,0.2)]`}
68+
className={`relative ${sizeClass} filter drop-shadow-[4px_4px_0_rgba(0,0,0,0.2)]`}
6969
style={{
7070
animation: 'float 2s steps(2) infinite',
7171
animationDelay: `${animationDelay}s`,
7272
}}
7373
>
74-
<Image
75-
src={getBadgeImage(badge)}
76-
alt={badge.name}
77-
fill
78-
className="object-contain"
79-
/>
74+
{/* 光の粒エフェクト(ランクバッジのみ) */}
75+
{!isTrophy && badge.category && (
76+
<>
77+
{[...Array(16)].map((_, i) => {
78+
const sparkleColor = CATEGORY_COLORS[badge.category!];
79+
// 不規則な遅延とポジション
80+
const delays = [
81+
0, 0.3, 0.7, 1.1, 0.5, 0.9, 1.3, 0.2, 0.8, 1.0, 0.4,
82+
1.2, 0.6, 1.4, 0.1, 1.5,
83+
];
84+
const positions = [
85+
5, 15, 25, 35, 45, 55, 65, 75, 10, 20, 30, 40, 50, 60,
86+
70, 80,
87+
];
88+
const delay = delays[i];
89+
const leftPosition = positions[i];
90+
91+
return (
92+
<div
93+
key={i}
94+
className="absolute w-6 h-6 rounded-full"
95+
style={{
96+
backgroundColor: sparkleColor,
97+
left: `${leftPosition}%`,
98+
bottom: 0,
99+
opacity: 0.7,
100+
boxShadow: `0 0 10px ${sparkleColor}`,
101+
animation: "sparkle-rise 3s ease-in-out infinite",
102+
animationDelay: `${delay}s`,
103+
zIndex: 1,
104+
}}
105+
/>
106+
);
107+
})}
108+
</>
109+
)}
110+
111+
{/* バッジ画像 */}
112+
<div className="relative w-full h-full" style={{ zIndex: 2 }}>
113+
<Image
114+
src={badge.image}
115+
alt={badge.name}
116+
fill
117+
className="object-contain"
118+
/>
119+
</div>
80120
</div>
81121
<span className="mt-2 text-center text-sm font-medium text-[#1a4023]">
82122
{badge.name}

frontend/src/features/grades/types/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export interface GradeStats {
1515
export interface Badge {
1616
id: string;
1717
name: string;
18-
icon: string;
19-
type: 'trophy' | 'gold' | 'silver' | 'bronze';
18+
type: 'trophy' | 'rank';
19+
image: string; // 画像パス
20+
category?: 'web' | 'ai' | 'security' | 'infra' | 'design';
21+
rankLevel?: number; // 0-9
22+
sortOrder: number; // トロフィー=1, 初級=2, 中級=3, 上級=4
2023
earnedAt?: string;
2124
}

0 commit comments

Comments
 (0)