Skip to content

Commit 5a8c64e

Browse files
Merge pull request #109 from kc3hack/feature/issue-101-badge-redesign
feat: #101 バッジデザインの変更
2 parents 7d7b3cf + 8235efd commit 5a8c64e

File tree

2 files changed

+94
-15
lines changed

2 files changed

+94
-15
lines changed

frontend/src/app/globals.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ body {
9999
}
100100
}
101101

102+
@keyframes sparkle-rise {
103+
0% {
104+
transform: translateY(0) scale(0);
105+
opacity: 0;
106+
}
107+
10% {
108+
opacity: 1;
109+
}
110+
90% {
111+
opacity: 1;
112+
}
113+
100% {
114+
transform: translateY(-230px) scale(1);
115+
opacity: 0;
116+
}
117+
}
118+
102119
.animate-fadeIn {
103120
animation: fadeIn 1s ease-out forwards;
104121
}

frontend/src/features/dashboard/components/AcquiredBadges.tsx

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,43 @@
22

33
import Image from 'next/image';
44

5+
// カテゴリ色定義
6+
const CATEGORY_COLORS: Record<string, string> = {
7+
web: "#55aaff",
8+
ai: "#e8b849",
9+
security: "#e85555",
10+
infra: "#55cc55",
11+
design: "#cc66dd"
12+
};
13+
14+
interface Badge {
15+
id: number;
16+
name: string;
17+
type: 'trophy' | 'rank';
18+
image: string;
19+
category?: keyof typeof CATEGORY_COLORS;
20+
rankLevel?: number;
21+
sortOrder: number; // トロフィー=1, 初級=2, 中級=3, 上級=4
22+
}
23+
524
export function AcquiredBadges() {
6-
const badges = [
7-
{ id: 1, name: 'Trophy', image: '/images/badges/Trophy.png' },
8-
{ id: 2, name: 'AI Basic', image: '/images/badges/AI_basic.png' },
9-
{ id: 3, name: 'Web Basic', image: '/images/badges/Web_base.png' },
10-
{ id: 4, name: 'Seed', image: '/images/badges/Seed.png' },
11-
{ id: 5, name: 'AI Master', image: '/images/badges/AI_basic.png' },
12-
{ id: 6, name: 'Web Advanced', image: '/images/badges/Web_base.png' },
25+
const badges: Badge[] = [
26+
{ id: 1, name: 'Trophy', type: 'trophy', image: '/images/badges/Trophy.png', sortOrder: 1 },
27+
{ id: 2, name: 'AI Basic', type: 'rank', image: '/images/ranks/rank_tree_1.png', category: 'ai', rankLevel: 1, sortOrder: 2 },
28+
{ id: 3, name: 'Web Basic', type: 'rank', image: '/images/ranks/rank_tree_1.png', category: 'web', rankLevel: 1, sortOrder: 2 },
29+
{ id: 4, name: 'Security Basic', type: 'rank', image: '/images/ranks/rank_tree_1.png', category: 'security', rankLevel: 1, sortOrder: 2 },
30+
{ id: 5, name: 'AI Intermediate', type: 'rank', image: '/images/ranks/rank_tree_3.png', category: 'ai', rankLevel: 3, sortOrder: 3 },
31+
{ id: 6, name: 'Web Advanced', type: 'rank', image: '/images/ranks/rank_tree_5.png', category: 'web', rankLevel: 5, sortOrder: 4 },
1332
];
1433

34+
// ソート: トロフィー → 初級 → 中級 → 上級
35+
const sortedBadges = [...badges].sort((a, b) => {
36+
if (a.sortOrder !== b.sortOrder) {
37+
return a.sortOrder - b.sortOrder;
38+
}
39+
return a.id - b.id;
40+
});
41+
1542
return (
1643
<div className="mt-8 font-sans">
1744
<h3 className="mb-4 text-2xl font-bold tracking-widest text-[#2C5F2D] [text-shadow:2px_2px_0_#a3e635]">
@@ -28,10 +55,11 @@ export function AcquiredBadges() {
2855
>
2956
{/* Badges in horizontal scroll */}
3057
<div className="flex gap-6 min-w-max items-end pb-4">
31-
{badges.map((badge, index) => {
32-
const isTrophy = badge.name === 'Trophy';
58+
{sortedBadges.map((badge, index) => {
59+
const isTrophy = badge.type === 'trophy';
3360
const sizeClass = isTrophy ? 'h-80 w-80' : 'h-60 w-60';
3461
const animationDelay = index * 0.2;
62+
const backgroundColor = badge.category ? CATEGORY_COLORS[badge.category] : 'transparent';
3563

3664
return (
3765
<div
@@ -46,12 +74,46 @@ export function AcquiredBadges() {
4674
animationDelay: `${animationDelay}s`,
4775
}}
4876
>
49-
<Image
50-
src={badge.image}
51-
alt={badge.name}
52-
fill
53-
className="object-contain"
54-
/>
77+
{/* 光の粒エフェクト(ランクバッジのみ) */}
78+
{!isTrophy && badge.category && (
79+
<>
80+
{[...Array(16)].map((_, i) => {
81+
const sparkleColor = CATEGORY_COLORS[badge.category!];
82+
// 不規則な遅延とポジション
83+
const delays = [0, 0.3, 0.7, 1.1, 0.5, 0.9, 1.3, 0.2, 0.8, 1.0, 0.4, 1.2, 0.6, 1.4, 0.1, 1.5];
84+
const positions = [5, 15, 25, 35, 45, 55, 65, 75, 10, 20, 30, 40, 50, 60, 70, 80];
85+
const delay = delays[i];
86+
const leftPosition = positions[i];
87+
88+
return (
89+
<div
90+
key={i}
91+
className="absolute w-6 h-6 rounded-full"
92+
style={{
93+
backgroundColor: sparkleColor,
94+
left: `${leftPosition}%`,
95+
bottom: 0,
96+
opacity: 0.7,
97+
boxShadow: `0 0 10px ${sparkleColor}`,
98+
animation: 'sparkle-rise 3s ease-in-out infinite',
99+
animationDelay: `${delay}s`,
100+
zIndex: 1,
101+
}}
102+
/>
103+
);
104+
})}
105+
</>
106+
)}
107+
108+
{/* バッジ画像 */}
109+
<div className="relative w-full h-full" style={{ zIndex: 2 }}>
110+
<Image
111+
src={badge.image}
112+
alt={badge.name}
113+
fill
114+
className="object-contain"
115+
/>
116+
</div>
55117
</div>
56118
</div>
57119
);

0 commit comments

Comments
 (0)