-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Feature-based Architecture採用によるダッシュボード機能の実装 #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,17 @@ | ||||||||||||
| /** | ||||||||||||
| * Dashboard Layout | ||||||||||||
| * ダッシュボード機能用のレイアウト(ロジックはなく、フロートのみ) | ||||||||||||
| */ | ||||||||||||
|
|
||||||||||||
| export const metadata = { | ||||||||||||
|
Comment on lines
+5
to
+6
|
||||||||||||
| export const metadata = { | |
| import type { Metadata } from 'next'; | |
| export const metadata: Metadata = { |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /** | ||
| * Dashboard Page | ||
| * ダッシュボード機能のメインページ | ||
| */ | ||
|
|
||
| import { DashboardContainer } from '@/features/dashboard/components/DashboardContainer'; | ||
|
|
||
| export default function DashboardPage() { | ||
| return <DashboardContainer userId="user-123" />; | ||
|
Comment on lines
+6
to
+9
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ | |
| }); | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: "Create Next App", | ||
| description: "Generated by create next app", | ||
| title: "Dashboard | Skill Tree", | ||
| description: "Your personal skill development dashboard", | ||
| }; | ||
|
Comment on lines
15
to
18
|
||
|
|
||
| export default function RootLayout({ | ||
|
|
@@ -23,7 +23,7 @@ export default function RootLayout({ | |
| children: React.ReactNode; | ||
| }>) { | ||
| return ( | ||
| <html lang="en"> | ||
| <html lang="ja"> | ||
| <body | ||
|
Comment on lines
+26
to
27
|
||
| className={`${geistSans.variable} ${geistMono.variable} antialiased`} | ||
| > | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /** | ||
| * Dashboard Feature Mock Data | ||
| * この機能専用のモックデータを返す関数 | ||
| */ | ||
|
|
||
| import type { UserStatus, Badge, SkillNode, Rank } from '../types'; | ||
|
|
||
| const mockBadges: Badge[] = [ | ||
| { | ||
| id: 'badge-1', | ||
| name: 'Beginner', | ||
| icon: '🌱', | ||
| description: '初心者バッジ', | ||
| unlockedAt: new Date('2024-01-15'), | ||
| }, | ||
| { | ||
| id: 'badge-2', | ||
| name: 'Explorer', | ||
| icon: '🧭', | ||
| description: 'エクスプローラーバッジ', | ||
| unlockedAt: new Date('2024-02-20'), | ||
| }, | ||
| { | ||
| id: 'badge-3', | ||
| name: 'Master', | ||
| icon: '⭐', | ||
| description: 'マスターバッジ', | ||
| }, | ||
| ]; | ||
|
|
||
| const mockSkills: SkillNode[] = [ | ||
| { | ||
| id: 'skill-1', | ||
| name: 'TypeScript基礎', | ||
| description: 'TypeScriptの基本を学ぶ', | ||
| icon: '📘', | ||
| completed: true, | ||
| level: 5, | ||
| prerequisites: [], | ||
| }, | ||
| { | ||
| id: 'skill-2', | ||
| name: 'React基礎', | ||
| description: 'Reactの基本を学ぶ', | ||
| icon: '⚛️', | ||
| completed: true, | ||
| level: 4, | ||
| prerequisites: ['skill-1'], | ||
| }, | ||
| { | ||
| id: 'skill-3', | ||
| name: 'Next.js応用', | ||
| description: 'Next.jsの応用技術を学ぶ', | ||
| icon: '🚀', | ||
| completed: true, | ||
| level: 3, | ||
| prerequisites: ['skill-2'], | ||
| }, | ||
| { | ||
| id: 'skill-4', | ||
| name: 'デプロイメント', | ||
| description: 'アプリケーションのデプロイ方法を学ぶ', | ||
| icon: '🛸', | ||
| completed: false, | ||
| level: 1, | ||
| prerequisites: ['skill-3'], | ||
| }, | ||
| ]; | ||
|
|
||
| const mockRank: Rank = { | ||
| level: 12, | ||
| title: 'Senior Developer', | ||
| progress: 65, | ||
| nextLevelExp: 5000, | ||
| }; | ||
|
|
||
| /** | ||
| * ユーザーのダッシュボード情報を取得(モック) | ||
| */ | ||
| export async function fetchUserDashboard(userId: string): Promise<UserStatus> { | ||
| // 実装では、ここでバックエンドAPIを呼び出す | ||
| // await fetch(`/api/users/${userId}/dashboard`) | ||
| return { | ||
| userId, | ||
| displayName: 'Sample User', | ||
| avatar: '👨💻', | ||
| totalExp: 12300, | ||
| currentRank: mockRank, | ||
| badges: mockBadges, | ||
| skillRoadmap: mockSkills, | ||
| joinedAt: new Date('2023-06-15'), | ||
| lastActivityAt: new Date(), | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| 'use client'; | ||
|
|
||
| /** | ||
| * BadgeList Component | ||
| * 獲得したバッジ一覧と未獲得バッジを表示 | ||
| */ | ||
|
|
||
| import type { Badge } from '../types'; | ||
|
|
||
| interface BadgeListProps { | ||
| badges: Badge[]; | ||
| } | ||
|
|
||
| export function BadgeList({ badges }: BadgeListProps) { | ||
| const unlockedBadges = badges.filter((badge) => badge.unlockedAt); | ||
| const lockedBadges = badges.filter((badge) => !badge.unlockedAt); | ||
|
|
||
| return ( | ||
| <div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-900"> | ||
| <h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white"> | ||
| 🏅 バッジ | ||
| </h2> | ||
|
|
||
| {/* 獲得済みバッジ */} | ||
| {unlockedBadges.length > 0 && ( | ||
| <div className="mb-6"> | ||
| <h3 className="mb-3 text-sm font-semibold uppercase text-gray-600 dark:text-gray-400"> | ||
| 獲得済み({unlockedBadges.length}) | ||
| </h3> | ||
| <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4"> | ||
| {unlockedBadges.map((badge) => ( | ||
| <div | ||
| key={badge.id} | ||
| 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" | ||
| > | ||
| <div className="text-4xl">{badge.icon}</div> | ||
| <p className="mt-2 text-sm font-medium text-gray-900 dark:text-white"> | ||
| {badge.name} | ||
| </p> | ||
| <p className="mt-1 text-xs text-gray-600 dark:text-gray-400"> | ||
| {new Date(badge.unlockedAt!).toLocaleDateString('ja-JP')} | ||
| </p> | ||
|
Comment on lines
+39
to
+42
|
||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* 未獲得バッジ */} | ||
| {lockedBadges.length > 0 && ( | ||
| <div> | ||
| <h3 className="mb-3 text-sm font-semibold uppercase text-gray-600 dark:text-gray-400"> | ||
| 未獲得({lockedBadges.length}) | ||
| </h3> | ||
| <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4"> | ||
| {lockedBadges.map((badge) => ( | ||
| <div | ||
| key={badge.id} | ||
| className="flex flex-col items-center rounded-lg bg-gray-100 p-4 opacity-50 dark:bg-gray-800" | ||
| > | ||
| <div className="text-4xl opacity-50">{badge.icon}</div> | ||
| <p className="mt-2 text-sm font-medium text-gray-500 dark:text-gray-400"> | ||
| {badge.name} | ||
| </p> | ||
| <p className="mt-1 text-xs text-gray-500 dark:text-gray-500"> | ||
| 近日対応 | ||
| </p> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * DashboardContainer Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ダッシュボードの全コンポーネントを統合し、データフェッチを担当 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { UserStatus } from '../types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { fetchUserDashboard } from '../api/mock'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { StatusCard } from './StatusCard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BadgeList } from './BadgeList'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SkillRoadmap } from './SkillRoadmap'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface DashboardContainerProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function DashboardContainer({ userId = 'default-user' }: DashboardContainerProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [userStatus, setUserStatus] = useState<UserStatus | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const loadDashboard = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+25
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function DashboardContainer({ userId = 'default-user' }: DashboardContainerProps) { | |
| const [userStatus, setUserStatus] = useState<UserStatus | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| useEffect(() => { | |
| const loadDashboard = async () => { | |
| export function DashboardContainer({ userId }: DashboardContainerProps) { | |
| const [userStatus, setUserStatus] = useState<UserStatus | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| useEffect(() => { | |
| const loadDashboard = async () => { | |
| // userId が未指定の場合は安全側に倒してエラー扱いとし、API 呼び出しを行わない | |
| if (!userId) { | |
| setError('ユーザーIDが指定されていません'); | |
| setUserStatus(null); | |
| setLoading(false); | |
| return; | |
| } |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect 内の非同期処理がコンポーネントのアンマウント後も setState し得るため、Reactの警告や不要な更新の原因になります。クリーンアップでキャンセルフラグ(または AbortController)を扱い、アンマウント後は state 更新しないようにしてください。
| const loadDashboard = async () => { | |
| try { | |
| setLoading(true); | |
| const data = await fetchUserDashboard(userId); | |
| setUserStatus(data); | |
| setError(null); | |
| } catch (err) { | |
| setError( | |
| err instanceof Error ? err.message : 'ダッシュボードの読み込みに失敗しました' | |
| ); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| loadDashboard(); | |
| let isCancelled = false; | |
| const loadDashboard = async () => { | |
| try { | |
| setLoading(true); | |
| const data = await fetchUserDashboard(userId); | |
| if (isCancelled) { | |
| return; | |
| } | |
| setUserStatus(data); | |
| setError(null); | |
| } catch (err) { | |
| if (isCancelled) { | |
| return; | |
| } | |
| setError( | |
| err instanceof Error ? err.message : 'ダッシュボードの読み込みに失敗しました' | |
| ); | |
| } finally { | |
| if (!isCancelled) { | |
| setLoading(false); | |
| } | |
| } | |
| }; | |
| loadDashboard(); | |
| return () => { | |
| isCancelled = true; | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
コメントの「フロートのみ」は表現として意味が取りづらく、意図(例: 「表示のみ」「レイアウトのみ」「ラッパーのみ」等)が伝わりません。内容に合う語に修正して、読む人が誤解しないようにしてください。