Skip to content

Commit 75eb350

Browse files
Merge pull request #34 from kc3hack/feature/issue-23-ui-mock
feat: Feature-based Architecture採用によるダッシュボード機能の実装
2 parents 54cb6fe + 1cc6571 commit 75eb350

File tree

9 files changed

+492
-3
lines changed

9 files changed

+492
-3
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Dashboard Layout
3+
* ダッシュボード機能用のレイアウト(ロジックはなく、フロートのみ)
4+
*/
5+
6+
export const metadata = {
7+
title: 'Dashboard',
8+
description: 'Your personal dashboard',
9+
};
10+
11+
export default function DashboardLayout({
12+
children,
13+
}: {
14+
children: React.ReactNode;
15+
}) {
16+
return <>{children}</>;
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Dashboard Page
3+
* ダッシュボード機能のメインページ
4+
*/
5+
6+
import { DashboardContainer } from '@/features/dashboard/components/DashboardContainer';
7+
8+
export default function DashboardPage() {
9+
return <DashboardContainer userId="user-123" />;
10+
}

frontend/src/app/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
1313
});
1414

1515
export const metadata: Metadata = {
16-
title: "Create Next App",
17-
description: "Generated by create next app",
16+
title: "Dashboard | Skill Tree",
17+
description: "Your personal skill development dashboard",
1818
};
1919

2020
export default function RootLayout({
@@ -23,7 +23,7 @@ export default function RootLayout({
2323
children: React.ReactNode;
2424
}>) {
2525
return (
26-
<html lang="en">
26+
<html lang="ja">
2727
<body
2828
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
2929
>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Dashboard Feature Mock Data
3+
* この機能専用のモックデータを返す関数
4+
*/
5+
6+
import type { UserStatus, Badge, SkillNode, Rank } from '../types';
7+
8+
const mockBadges: Badge[] = [
9+
{
10+
id: 'badge-1',
11+
name: 'Beginner',
12+
icon: '🌱',
13+
description: '初心者バッジ',
14+
unlockedAt: new Date('2024-01-15'),
15+
},
16+
{
17+
id: 'badge-2',
18+
name: 'Explorer',
19+
icon: '🧭',
20+
description: 'エクスプローラーバッジ',
21+
unlockedAt: new Date('2024-02-20'),
22+
},
23+
{
24+
id: 'badge-3',
25+
name: 'Master',
26+
icon: '⭐',
27+
description: 'マスターバッジ',
28+
},
29+
];
30+
31+
const mockSkills: SkillNode[] = [
32+
{
33+
id: 'skill-1',
34+
name: 'TypeScript基礎',
35+
description: 'TypeScriptの基本を学ぶ',
36+
icon: '📘',
37+
completed: true,
38+
level: 5,
39+
prerequisites: [],
40+
},
41+
{
42+
id: 'skill-2',
43+
name: 'React基礎',
44+
description: 'Reactの基本を学ぶ',
45+
icon: '⚛️',
46+
completed: true,
47+
level: 4,
48+
prerequisites: ['skill-1'],
49+
},
50+
{
51+
id: 'skill-3',
52+
name: 'Next.js応用',
53+
description: 'Next.jsの応用技術を学ぶ',
54+
icon: '🚀',
55+
completed: true,
56+
level: 3,
57+
prerequisites: ['skill-2'],
58+
},
59+
{
60+
id: 'skill-4',
61+
name: 'デプロイメント',
62+
description: 'アプリケーションのデプロイ方法を学ぶ',
63+
icon: '🛸',
64+
completed: false,
65+
level: 1,
66+
prerequisites: ['skill-3'],
67+
},
68+
];
69+
70+
const mockRank: Rank = {
71+
level: 12,
72+
title: 'Senior Developer',
73+
progress: 65,
74+
nextLevelExp: 5000,
75+
};
76+
77+
/**
78+
* ユーザーのダッシュボード情報を取得(モック)
79+
*/
80+
export async function fetchUserDashboard(userId: string): Promise<UserStatus> {
81+
// 実装では、ここでバックエンドAPIを呼び出す
82+
// await fetch(`/api/users/${userId}/dashboard`)
83+
return {
84+
userId,
85+
displayName: 'Sample User',
86+
avatar: '👨‍💻',
87+
totalExp: 12300,
88+
currentRank: mockRank,
89+
badges: mockBadges,
90+
skillRoadmap: mockSkills,
91+
joinedAt: new Date('2023-06-15'),
92+
lastActivityAt: new Date(),
93+
};
94+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client';
2+
3+
/**
4+
* BadgeList Component
5+
* 獲得したバッジ一覧と未獲得バッジを表示
6+
*/
7+
8+
import type { Badge } from '../types';
9+
10+
interface BadgeListProps {
11+
badges: Badge[];
12+
}
13+
14+
export function BadgeList({ badges }: BadgeListProps) {
15+
const unlockedBadges = badges.filter((badge) => badge.unlockedAt);
16+
const lockedBadges = badges.filter((badge) => !badge.unlockedAt);
17+
18+
return (
19+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-900">
20+
<h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white">
21+
🏅 バッジ
22+
</h2>
23+
24+
{/* 獲得済みバッジ */}
25+
{unlockedBadges.length > 0 && (
26+
<div className="mb-6">
27+
<h3 className="mb-3 text-sm font-semibold uppercase text-gray-600 dark:text-gray-400">
28+
獲得済み({unlockedBadges.length}
29+
</h3>
30+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
31+
{unlockedBadges.map((badge) => (
32+
<div
33+
key={badge.id}
34+
className="flex flex-col items-center rounded-lg bg-gradient-to-br from-yellow-50 to-orange-50 p-4 transition-transform hover:scale-105 dark:from-yellow-900/20 dark:to-orange-900/20"
35+
>
36+
<div className="text-4xl">{badge.icon}</div>
37+
<p className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
38+
{badge.name}
39+
</p>
40+
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
41+
{new Date(badge.unlockedAt!).toLocaleDateString('ja-JP')}
42+
</p>
43+
</div>
44+
))}
45+
</div>
46+
</div>
47+
)}
48+
49+
{/* 未獲得バッジ */}
50+
{lockedBadges.length > 0 && (
51+
<div>
52+
<h3 className="mb-3 text-sm font-semibold uppercase text-gray-600 dark:text-gray-400">
53+
未獲得({lockedBadges.length}
54+
</h3>
55+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
56+
{lockedBadges.map((badge) => (
57+
<div
58+
key={badge.id}
59+
className="flex flex-col items-center rounded-lg bg-gray-100 p-4 opacity-50 dark:bg-gray-800"
60+
>
61+
<div className="text-4xl opacity-50">{badge.icon}</div>
62+
<p className="mt-2 text-sm font-medium text-gray-500 dark:text-gray-400">
63+
{badge.name}
64+
</p>
65+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-500">
66+
近日対応
67+
</p>
68+
</div>
69+
))}
70+
</div>
71+
</div>
72+
)}
73+
</div>
74+
);
75+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use client';
2+
3+
/**
4+
* DashboardContainer Component
5+
* ダッシュボードの全コンポーネントを統合し、データフェッチを担当
6+
*/
7+
8+
import { useEffect, useState } from 'react';
9+
import type { UserStatus } from '../types';
10+
import { fetchUserDashboard } from '../api/mock';
11+
import { StatusCard } from './StatusCard';
12+
import { BadgeList } from './BadgeList';
13+
import { SkillRoadmap } from './SkillRoadmap';
14+
15+
interface DashboardContainerProps {
16+
userId?: string;
17+
}
18+
19+
export function DashboardContainer({ userId = 'default-user' }: DashboardContainerProps) {
20+
const [userStatus, setUserStatus] = useState<UserStatus | null>(null);
21+
const [loading, setLoading] = useState(true);
22+
const [error, setError] = useState<string | null>(null);
23+
24+
useEffect(() => {
25+
const loadDashboard = async () => {
26+
try {
27+
setLoading(true);
28+
const data = await fetchUserDashboard(userId);
29+
setUserStatus(data);
30+
setError(null);
31+
} catch (err) {
32+
setError(
33+
err instanceof Error ? err.message : 'ダッシュボードの読み込みに失敗しました'
34+
);
35+
} finally {
36+
setLoading(false);
37+
}
38+
};
39+
40+
loadDashboard();
41+
}, [userId]);
42+
43+
if (loading) {
44+
return (
45+
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
46+
<div className="text-center">
47+
<div className="mb-4 inline-flex h-12 w-12 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 dark:border-gray-700 dark:border-t-blue-400" />
48+
<p className="text-gray-600 dark:text-gray-400">
49+
ダッシュボードを読み込み中...
50+
</p>
51+
</div>
52+
</div>
53+
);
54+
}
55+
56+
if (error || !userStatus) {
57+
return (
58+
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
59+
<div className="rounded-lg bg-white p-8 text-center shadow-lg dark:bg-gray-800">
60+
<p className="text-red-600 dark:text-red-400">
61+
{error || 'ダッシュボードの読み込みに失敗しました'}
62+
</p>
63+
</div>
64+
</div>
65+
);
66+
}
67+
68+
return (
69+
<main className="min-h-screen bg-gray-50 px-4 py-8 dark:bg-gray-900">
70+
<div className="mx-auto max-w-6xl space-y-8">
71+
{/* ステータスカード */}
72+
<StatusCard userStatus={userStatus} />
73+
74+
{/* バッジ一覧 */}
75+
<BadgeList badges={userStatus.badges} />
76+
77+
{/* スキルロードマップ */}
78+
<SkillRoadmap skills={userStatus.skillRoadmap} />
79+
</div>
80+
</main>
81+
);
82+
}

0 commit comments

Comments
 (0)