Skip to content

Commit 85d4493

Browse files
authored
Merge pull request #63 from kc3hack/feature/issue-62-grades-display
feat: 成績表示ページの実装 (Frontend)
2 parents 7900399 + e1d4f6f commit 85d4493

File tree

7 files changed

+239
-0
lines changed

7 files changed

+239
-0
lines changed

frontend/src/app/grades/layout.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { AppSidebar } from '@/features/dashboard/components/AppSidebar';
2+
3+
export const metadata = {
4+
title: 'Grades',
5+
description: 'Your learning grades',
6+
};
7+
8+
export default function GradeLayout({
9+
children,
10+
}: {
11+
children: React.ReactNode;
12+
}) {
13+
return (
14+
<div className="min-h-screen bg-[#FDFEF0]">
15+
{/* Top Header Bar - Full Width */}
16+
<header className="sticky top-0 z-50 flex h-16 w-full items-center justify-end bg-[#559C71] px-6 text-white shadow-md">
17+
<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">
18+
{/* User Icon Placeholder */}
19+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-gray-400">
20+
<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" />
21+
</svg>
22+
</div>
23+
</header>
24+
25+
<div className="flex">
26+
<AppSidebar />
27+
28+
{/* Main Content Area */}
29+
<div className="flex-1 min-h-[calc(100vh-4rem)] bg-[#FDFEF0] sm:ml-64">
30+
{children}
31+
</div>
32+
</div>
33+
</div>
34+
);
35+
}

frontend/src/app/grades/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client';
2+
3+
/**
4+
* Grade Page
5+
* 成績を表示するページ
6+
*/
7+
8+
import React from 'react';
9+
import { GradesContainer } from '@/features/grades/components/GradesContainer';
10+
11+
export default function GradePage() {
12+
return (
13+
<div className="h-full w-full">
14+
<GradesContainer />
15+
</div>
16+
);
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Badge, GradeStats } from "../types";
2+
3+
export const getGradeStats = async (): Promise<GradeStats> => {
4+
// TODO: Replace with actual API call
5+
return {
6+
consecutiveDays: 365,
7+
completedQuests: 365
8+
};
9+
};
10+
11+
export const getBadges = async (): Promise<Badge[]> => {
12+
// TODO: Replace with actual API call
13+
return [
14+
{ id: '1', name: 'Master', icon: '🏆', type: 'trophy', earnedAt: '2025-01-01' },
15+
{ id: '2', name: 'Gold', icon: '🥇', type: 'gold', earnedAt: '2025-01-02' },
16+
{ id: '3', name: 'Silver', icon: '🥈', type: 'silver', earnedAt: '2025-01-03' },
17+
{ id: '4', name: 'Silver', icon: '🥈', type: 'silver', earnedAt: '2025-01-04' },
18+
{ id: '5', name: 'Bronze', icon: '🥉', type: 'bronze', earnedAt: '2025-01-05' },
19+
{ id: '6', name: 'Bronze', icon: '🥉', type: 'bronze', earnedAt: '2025-01-06' },
20+
{ id: '7', name: 'Bronze', icon: '🥉', type: 'bronze', earnedAt: '2025-01-07' },
21+
];
22+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Badge } from '../types';
5+
6+
interface BadgeListProps {
7+
badges: Badge[];
8+
}
9+
10+
export const BadgeList: React.FC<BadgeListProps> = ({ badges }) => {
11+
return (
12+
<div className="w-full max-w-5xl mx-auto px-4">
13+
<div className="flex items-center gap-4 mb-4 pl-2">
14+
<h2 className="text-xl font-medium text-[#1a4023]">取得したバッチ</h2>
15+
<div className="relative">
16+
<select className="text-gray-700 appearance-none bg-white border border-border-[#1a4023] rounded px-4 py-1 pr-8 text-sm focus:outline-none ">
17+
<option>全て</option>
18+
<option>座学</option>
19+
<option>実技</option>
20+
</select>
21+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
22+
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
23+
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
24+
</svg>
25+
</div>
26+
</div>
27+
</div>
28+
29+
<div className="border-2 border-[#1a4023] rounded-[40px] bg-[#f8fff8] p-8 lg:p-12 min-h-[250px] lg:min-h-[300px] flex items-center justify-around relative overflow-hidden">
30+
{/* Large Trophy on the left */}
31+
<div className="flex flex-col items-center justify-center transform scale-125 lg:scale-150">
32+
<span className="text-[100px] lg:text-[120px] drop-shadow-lg filter">🏆</span>
33+
</div>
34+
35+
{/* Medals on the right */}
36+
<div className="flex gap-8 lg:gap-12 items-end">
37+
{/* Gold - 1st */}
38+
<div className="flex flex-col items-center">
39+
<div className="relative">
40+
<span className="text-[60px] lg:text-[80px] drop-shadow-md">🥇</span>
41+
42+
</div>
43+
</div>
44+
45+
{/* Silver - 2nd */}
46+
<div className="flex flex-col items-center">
47+
<div className="relative">
48+
<span className="text-[60px] lg:text-[80px] drop-shadow-md">🥈</span>
49+
50+
</div>
51+
</div>
52+
53+
{/* Bronze - 3rd */}
54+
<div className="flex flex-col items-center">
55+
<div className="relative">
56+
<span className="text-[60px] lg:text-[80px] drop-shadow-md">🥉</span>
57+
58+
</div>
59+
</div>
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
3+
import React, { useEffect, useState } from 'react';
4+
import { StatusCard } from './StatusCard';
5+
import { BadgeList } from './BadgeList';
6+
import { getGradeStats, getBadges } from '../api/mock';
7+
import type { GradeStats, Badge } from '../types';
8+
9+
export const GradesContainer: React.FC = () => {
10+
const [stats, setStats] = useState<GradeStats | null>(null);
11+
const [badges, setBadges] = useState<Badge[]>([]);
12+
const [loading, setLoading] = useState(true);
13+
14+
useEffect(() => {
15+
const fetchData = async () => {
16+
try {
17+
const [statsData, badgesData] = await Promise.all([
18+
getGradeStats(),
19+
getBadges(),
20+
]);
21+
setStats(statsData);
22+
setBadges(badgesData);
23+
} catch (error) {
24+
console.error('Failed to fetch grade data:', error);
25+
} finally {
26+
setLoading(false);
27+
}
28+
};
29+
30+
fetchData();
31+
}, []);
32+
33+
if (loading) {
34+
return (
35+
<div className="flex justify-center items-center h-full min-h-[400px]">
36+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#3ca0f6]"></div>
37+
</div>
38+
);
39+
}
40+
41+
if (!stats) {
42+
return <div>Failed to load data</div>;
43+
}
44+
45+
return (
46+
<div className="container mx-auto px-4 py-4 max-w-7xl">
47+
<div className="mt-8 mb-24">
48+
<StatusCard stats={stats} />
49+
</div>
50+
<BadgeList badges={badges} />
51+
</div>
52+
);
53+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { GradeStats } from '../types';
5+
6+
interface StatusCardProps {
7+
stats: GradeStats;
8+
}
9+
10+
export const StatusCard: React.FC<StatusCardProps> = ({ stats }) => {
11+
return (
12+
<div className="flex justify-around items-start w-full max-w-5xl mx-auto px-6 relative mt-16 pt-10">
13+
{/* Consecutive Days */}
14+
<div className="flex flex-col items-left min-w-[200px] relative">
15+
<p className="text-[#006400] text-3xl font-medium absolute -top-16 -left-20">連続記録</p>
16+
<div className="flex items-baseline mt-7">
17+
<span className="text-7xl lg:text-9xl leading-none font-medium text-[#006400] font-sans -tracking-wide pt-2">
18+
{stats.consecutiveDays}
19+
</span>
20+
<span className="text-2xl lg:text-3xl text-[#006400] font-medium ml-3 pb-2"></span>
21+
</div>
22+
</div>
23+
24+
{/* Completed Quests */}
25+
<div className="flex flex-col items-left min-w-[200px] relative">
26+
<p className="text-[#006400] text-3xl font-medium absolute -top-16 -left-15">修了した問題</p>
27+
<div className="flex items-baseline mt-7">
28+
<span className="text-7xl lg:text-9xl leading-none font-medium text-[#006400] font-sans -tracking-wide pt-2">
29+
{stats.completedQuests}
30+
</span>
31+
<span className="text-3xl text-[#006400] font-medium ml-3 pb-2"></span>
32+
</div>
33+
</div>
34+
</div>
35+
);
36+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface GradeStats {
2+
consecutiveDays: number;
3+
completedQuests: number;
4+
}
5+
6+
export interface Badge {
7+
id: string;
8+
name: string;
9+
icon: string;
10+
type: 'trophy' | 'gold' | 'silver' | 'bronze';
11+
earnedAt?: string;
12+
}

0 commit comments

Comments
 (0)