-
Notifications
You must be signed in to change notification settings - Fork 0
feat(frontend): 実習メニュー画面の更新とSkillTreeのロールバック #55
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
80de310
68b7ff5
95b1f28
1840d61
4e484bb
f46be0b
0638753
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,35 @@ | ||
| import { AppSidebar } from '@/features/dashboard/components/AppSidebar'; | ||
|
|
||
| export const metadata = { | ||
| title: 'Exercises', | ||
| description: 'Your coding exercises', | ||
| }; | ||
|
|
||
| export default function ExerciseLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <div className="min-h-screen bg-[#FDFEF0]"> | ||
| {/* Top Header Bar - Full Width */} | ||
| <header className="sticky top-0 z-50 flex h-16 w-full items-center justify-end bg-[#559C71] px-6 text-white shadow-md"> | ||
| <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white text-gray-600 shadow-sm cursor-pointer hover:bg-gray-100 transition-colors"> | ||
| {/* User Icon Placeholder */} | ||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-gray-400"> | ||
| <path fillRule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clipRule="evenodd" /> | ||
| </svg> | ||
| </div> | ||
| </header> | ||
|
|
||
| <div className="flex"> | ||
| <AppSidebar /> | ||
|
|
||
| {/* Main Content Area */} | ||
| <div className="flex-1 min-h-[calc(100vh-4rem)] bg-[#FDFEF0] sm:ml-64"> | ||
| {children} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| 'use client'; | ||
|
|
||
| /** | ||
| * Exercise Menu Page | ||
| */ | ||
|
|
||
| import React from 'react'; | ||
| import { ExerciseMenu } from '@/features/exercise/components/ExerciseMenu'; | ||
|
|
||
| export default function ExercisePage() { | ||
| return ( | ||
| <div className="h-full w-full"> | ||
| <ExerciseMenu /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { useEffect, useState } from 'react'; | |
| import type { UserStatus } from '../types'; | ||
| import { fetchUserDashboard } from '../api/mock'; | ||
| import { AcquiredBadges } from './AcquiredBadges'; | ||
| import { SkillRoadmap } from './SkillRoadmap'; | ||
|
|
||
| interface DashboardContainerProps { | ||
| userId?: string; | ||
|
|
@@ -80,15 +81,9 @@ export function DashboardContainer({ userId = 'default-user' }: DashboardContain | |
| </div> | ||
| </div> | ||
|
|
||
| {/* Tree Image Section */} | ||
| {/* Skill Tree Section */} | ||
| <div className="flex justify-center py-8"> | ||
| {/* Using a placeholder for the tree image based on the mockup description */} | ||
| <div className="relative h-64 w-64"> | ||
| {/* Fallback to a large tree emoji if no image provided */} | ||
| <div className="flex h-full w-full items-center justify-center text-[10rem]"> | ||
| 🌳 | ||
| </div> | ||
| </div> | ||
| <SkillRoadmap skills={userStatus.skillRoadmap} /> | ||
|
Comment on lines
+84
to
+86
|
||
| </div> | ||
|
|
||
| {/* Acquired Badges Section */} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| 'use client'; | ||
|
|
||
| /** | ||
| * ExerciseMenu Component | ||
| * 演習メニュー画面(タブとカードグリッド)を表示する | ||
| */ | ||
|
|
||
| import React, { useState } from 'react'; | ||
|
|
||
| type TabType = 'regular' | 'book' | 'terminal'; | ||
|
|
||
| const TAB_ITEMS = [ | ||
| { id: 'regular' as TabType, label: '通常演習', icon: '🎓' }, | ||
| { id: 'book' as TabType, label: '', icon: '📖' }, | ||
| { id: 'terminal' as TabType, label: '', icon: '💻' }, // terminal icon approximation | ||
| ]; | ||
|
|
||
| export function ExerciseMenu() { | ||
| const [activeTab, setActiveTab] = useState<TabType>('regular'); | ||
|
||
|
|
||
| const exercises = [ | ||
| { title: 'Web', image: '/images/exercises/Web.png' }, | ||
| { title: 'Mobile', image: '/images/exercises/Mobile.png' }, | ||
| { title: 'Network', image: '/images/exercises/Network.png' }, | ||
| { title: 'Game', image: '/images/exercises/Game.png' }, | ||
| { title: 'Design', image: '/images/exercises/Design.png' }, | ||
| { title: 'Infrastructure', image: '/images/exercises/Infrastructure.png' }, | ||
| { title: 'AI', image: '/images/exercises/ai.png' }, | ||
| { title: 'Security', image: '/images/exercises/Security.png' }, | ||
| { title: 'Coming Soon...', image: null }, | ||
| ]; | ||
|
|
||
| const items = exercises.map((exercise, i) => ({ | ||
| id: i, | ||
| title: exercise.title, | ||
| image: exercise.image, | ||
| })); | ||
|
|
||
| return ( | ||
| <div className="flex h-full flex-col"> | ||
| {/* Tab Navigation Area */} | ||
| <div className="flex items-end bg-[#559C71] px-4 pt-4"> | ||
| {TAB_ITEMS.map((tab) => { | ||
| const isActive = activeTab === tab.id; | ||
| return ( | ||
| <button | ||
| key={tab.id} | ||
| onClick={() => setActiveTab(tab.id)} | ||
| className={`mr-1 flex items-center rounded-t-lg px-6 py-2 transition-colors ${ | ||
| isActive | ||
| ? 'bg-[#FDFEF0] text-[#559C71]' // Active styling (matches bg) | ||
| : 'bg-[#6AB085] text-white hover:bg-[#7BC196]' // Inactive styling | ||
| }`} | ||
| > | ||
| <span className="text-xl">{tab.icon}</span> | ||
| {tab.label && <span className="ml-2 font-bold">{tab.label}</span>} | ||
| </button> | ||
| ); | ||
| })} | ||
| {/* Spacer to fill the rest of the bar if needed */} | ||
| <div className="flex-1 border-b border-[#559C71]"></div> | ||
| </div> | ||
|
|
||
| {/* Main Content Area */} | ||
| <div className="flex-1 bg-[#FDFEF0] p-8"> | ||
| <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3"> | ||
| {items.map((item) => ( | ||
| <button | ||
| key={item.id} | ||
| onClick={() => console.log(`Clicked ${item.title}`)} | ||
| className="flex aspect-square w-full flex-col items-center justify-center rounded-3xl border-2 border-[#3A7E56] bg-white p-4 shadow-sm transition-transform hover:scale-105 hover:shadow-md cursor-pointer overflow-hidden" | ||
| > | ||
| <div className="relative -mt-4 flex h-4/5 w-full items-center justify-center"> | ||
| {/* Placeholder for the badge image */} | ||
| {item.image ? ( | ||
| // eslint-disable-next-line @next/next/no-img-element | ||
| <img | ||
| src={item.image} | ||
| alt={item.title} | ||
| className="h-full w-full object-contain scale-[1.8]" | ||
| onError={(e) => { | ||
| e.currentTarget.style.display = 'none'; | ||
| e.currentTarget.parentElement?.querySelector('.fallback-icon')?.classList.remove('hidden'); | ||
| }} | ||
| /> | ||
| ) : null} | ||
|
|
||
| {/* Fallback Icon (initially hidden if item.image exists) */} | ||
| <div className={`fallback-icon h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center text-4xl ${item.image ? 'hidden' : ''}`}> | ||
| 🏆 | ||
| </div> | ||
| </div> | ||
| <h3 className="text-2xl font-bold text-[#1a4023] text-center w-full break-words"> | ||
| {item.title} | ||
| </h3> | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
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.
exercises/layout.tsxとdashboard/layout.tsxの間でレイアウトコードが重複しています。ヘッダーバー、サイドバー、メインコンテンツエリアの構造がほぼ同一です。共通のレイアウトコンポーネントを作成し、両方のレイアウトから再利用することで、保守性が向上し、将来的な変更が容易になります。例えば、AppLayoutコンポーネントを作成し、それを両方のレイアウトファイルから使用することを検討してください。
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.
別issue切り出し