Skip to content

Commit 7440586

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/implement-issue-67-animations-b
# Conflicts: # src/app/history/page.tsx
2 parents 2699e51 + ac0ef2a commit 7440586

28 files changed

Lines changed: 2524 additions & 19 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"react": "19.1.4",
3737
"react-day-picker": "^9.11.1",
3838
"react-dom": "19.1.4",
39+
"recharts": "^3.8.1",
3940
"tailwind-merge": "^3.3.1",
4041
"uuid": "^13.0.0",
4142
"zustand": "^5.0.8"

pnpm-lock.yaml

Lines changed: 353 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/analytics/page.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import {
6+
BookOpen,
7+
CalendarDays,
8+
Flame,
9+
Pencil,
10+
} from 'lucide-react';
11+
import { useAuth } from '@/hooks/useAuth';
12+
import { useStatistics } from '@/hooks/useStatistics';
13+
import Header from '@/components/layout/Header';
14+
import DashboardLoading from '@/app/dashboard/loading';
15+
import StatsCard from '@/components/statistics/StatsCard';
16+
import TrendChart from '@/components/statistics/TrendChart';
17+
import FrameworkBreakdown from '@/components/statistics/FrameworkBreakdown';
18+
import PeriodComparison from '@/components/statistics/PeriodComparison';
19+
import StreakDisplay from '@/components/statistics/StreakDisplay';
20+
import GrowthScore from '@/components/statistics/GrowthScore';
21+
22+
export default function AnalyticsPage() {
23+
const { user, signOut, isLoading: authLoading } = useAuth();
24+
const router = useRouter();
25+
const { summary, trends, distribution, isLoading, error, refetch } =
26+
useStatistics(user?.id);
27+
28+
useEffect(() => {
29+
if (!authLoading && !user) {
30+
const currentPath = `${window.location.pathname}${window.location.search}`;
31+
router.push(`/auth?next=${encodeURIComponent(currentPath)}`);
32+
}
33+
}, [user, authLoading, router]);
34+
35+
const handleSignOut = async () => {
36+
await signOut();
37+
router.push('/auth');
38+
};
39+
40+
if (authLoading || !user) {
41+
return <DashboardLoading />;
42+
}
43+
44+
return (
45+
<div className="min-h-screen bg-[#fafafa]">
46+
<Header
47+
isAuthenticated={!!user}
48+
userName={user.name}
49+
onSignOut={handleSignOut}
50+
title="統計・トレンド分析"
51+
showBackButton
52+
backHref="/dashboard"
53+
/>
54+
55+
<main className="max-w-7xl mx-auto px-4 py-8">
56+
{error && (
57+
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-center justify-between">
58+
<p className="text-red-800">{error}</p>
59+
<button
60+
type="button"
61+
onClick={() => refetch()}
62+
className="px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700 text-sm"
63+
>
64+
再読み込み
65+
</button>
66+
</div>
67+
)}
68+
69+
{isLoading && !summary ? (
70+
<div className="space-y-6">
71+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
72+
{[1, 2, 3, 4].map((i) => (
73+
<div
74+
key={i}
75+
className="h-28 bg-white rounded-lg border border-gray-200 animate-pulse"
76+
/>
77+
))}
78+
</div>
79+
<div className="h-80 bg-white rounded-lg border border-gray-200 animate-pulse" />
80+
</div>
81+
) : summary && trends && distribution ? (
82+
<div className="space-y-6">
83+
<section>
84+
<h2 className="text-lg font-semibold text-gray-900 mb-3">
85+
KPI サマリー
86+
</h2>
87+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
88+
<StatsCard
89+
label="総振り返り数"
90+
value={summary.basicStats.total}
91+
unit="件"
92+
icon={BookOpen}
93+
iconColor="text-blue-500"
94+
/>
95+
<StatsCard
96+
label="今月の振り返り"
97+
value={summary.basicStats.thisMonth}
98+
unit="件"
99+
icon={CalendarDays}
100+
iconColor="text-emerald-500"
101+
/>
102+
<StatsCard
103+
label="現在の連続日数"
104+
value={summary.streak.currentStreak}
105+
unit="日"
106+
icon={Flame}
107+
iconColor="text-orange-500"
108+
description={`ベスト: ${summary.streak.bestStreak} 日`}
109+
/>
110+
<StatsCard
111+
label="平均文字数"
112+
value={summary.basicStats.averageCharacters}
113+
unit="文字"
114+
icon={Pencil}
115+
iconColor="text-purple-500"
116+
description="1件あたりの平均"
117+
/>
118+
</div>
119+
</section>
120+
121+
<section className="grid grid-cols-1 lg:grid-cols-3 gap-4">
122+
<div className="lg:col-span-2">
123+
<TrendChart trends={trends} />
124+
</div>
125+
<GrowthScore score={summary.growthScore} />
126+
</section>
127+
128+
<section className="grid grid-cols-1 lg:grid-cols-2 gap-4">
129+
<PeriodComparison
130+
title="前月比"
131+
currentLabel="今月"
132+
previousLabel="先月"
133+
data={summary.monthComparison}
134+
/>
135+
<PeriodComparison
136+
title="前週比"
137+
currentLabel="今週"
138+
previousLabel="先週"
139+
data={summary.weekComparison}
140+
/>
141+
</section>
142+
143+
<section className="grid grid-cols-1 lg:grid-cols-2 gap-4">
144+
<StreakDisplay streak={summary.streak} />
145+
<FrameworkBreakdown distribution={distribution.frameworks} />
146+
</section>
147+
</div>
148+
) : null}
149+
</main>
150+
</div>
151+
);
152+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextResponse } from 'next/server';
2+
import { createClient } from '@/lib/supabase/server';
3+
import { getDistribution } from '@/services/analyticsService';
4+
import type { Reflection } from '@/types/reflection';
5+
import type { Framework } from '@/types/framework';
6+
7+
export async function GET() {
8+
try {
9+
const supabase = await createClient();
10+
const {
11+
data: { user },
12+
error: userError,
13+
} = await supabase.auth.getUser();
14+
15+
if (userError || !user) {
16+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
17+
}
18+
19+
const [reflectionsResult, frameworksResult] = await Promise.all([
20+
supabase.from('retrospectives').select('*').eq('user_id', user.id),
21+
supabase.from('frameworks').select('*').eq('is_active', true),
22+
]);
23+
24+
if (reflectionsResult.error) {
25+
return NextResponse.json(
26+
{ error: 'Failed to fetch reflections' },
27+
{ status: 500 },
28+
);
29+
}
30+
31+
if (frameworksResult.error) {
32+
return NextResponse.json(
33+
{ error: 'Failed to fetch frameworks' },
34+
{ status: 500 },
35+
);
36+
}
37+
38+
const frameworks: Framework[] = (frameworksResult.data || []).map((f) => ({
39+
...f,
40+
schema: f.schema?.fields || [],
41+
}));
42+
43+
const distribution = getDistribution(
44+
(reflectionsResult.data as Reflection[]) || [],
45+
frameworks,
46+
);
47+
48+
return NextResponse.json({ distribution });
49+
} catch {
50+
return NextResponse.json(
51+
{ error: 'Internal server error' },
52+
{ status: 500 },
53+
);
54+
}
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextResponse } from 'next/server';
2+
import { createClient } from '@/lib/supabase/server';
3+
import { getSummary } from '@/services/analyticsService';
4+
import type { Reflection } from '@/types/reflection';
5+
import type { Framework } from '@/types/framework';
6+
7+
export async function GET() {
8+
try {
9+
const supabase = await createClient();
10+
const {
11+
data: { user },
12+
error: userError,
13+
} = await supabase.auth.getUser();
14+
15+
if (userError || !user) {
16+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
17+
}
18+
19+
const [reflectionsResult, frameworksResult] = await Promise.all([
20+
supabase.from('retrospectives').select('*').eq('user_id', user.id),
21+
supabase.from('frameworks').select('*').eq('is_active', true),
22+
]);
23+
24+
if (reflectionsResult.error) {
25+
return NextResponse.json(
26+
{ error: 'Failed to fetch reflections' },
27+
{ status: 500 },
28+
);
29+
}
30+
31+
if (frameworksResult.error) {
32+
return NextResponse.json(
33+
{ error: 'Failed to fetch frameworks' },
34+
{ status: 500 },
35+
);
36+
}
37+
38+
const frameworks: Framework[] = (frameworksResult.data || []).map((f) => ({
39+
...f,
40+
schema: f.schema?.fields || [],
41+
}));
42+
43+
const summary = getSummary(
44+
(reflectionsResult.data as Reflection[]) || [],
45+
frameworks,
46+
);
47+
48+
return NextResponse.json({ summary });
49+
} catch {
50+
return NextResponse.json(
51+
{ error: 'Internal server error' },
52+
{ status: 500 },
53+
);
54+
}
55+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { createClient } from '@/lib/supabase/server';
3+
import { getTrends } from '@/services/analyticsService';
4+
import type { Reflection } from '@/types/reflection';
5+
6+
const clampInt = (value: string | null, fallback: number, min: number, max: number): number => {
7+
if (value === null) return fallback;
8+
const trimmed = value.trim();
9+
if (trimmed === '') return fallback;
10+
const parsed = Number(trimmed);
11+
if (!Number.isFinite(parsed)) return fallback;
12+
return Math.min(Math.max(Math.floor(parsed), min), max);
13+
};
14+
15+
export async function GET(request: NextRequest) {
16+
try {
17+
const supabase = await createClient();
18+
const {
19+
data: { user },
20+
error: userError,
21+
} = await supabase.auth.getUser();
22+
23+
if (userError || !user) {
24+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
25+
}
26+
27+
const { searchParams } = request.nextUrl;
28+
const weeks = clampInt(searchParams.get('weeks'), 12, 1, 52);
29+
const months = clampInt(searchParams.get('months'), 6, 1, 24);
30+
31+
const { data: reflections, error: reflectionsError } = await supabase
32+
.from('retrospectives')
33+
.select('*')
34+
.eq('user_id', user.id);
35+
36+
if (reflectionsError) {
37+
return NextResponse.json(
38+
{ error: 'Failed to fetch reflections' },
39+
{ status: 500 },
40+
);
41+
}
42+
43+
const trends = getTrends(
44+
(reflections as Reflection[]) || [],
45+
new Date(),
46+
{ weeks, months },
47+
);
48+
49+
return NextResponse.json({ trends });
50+
} catch {
51+
return NextResponse.json(
52+
{ error: 'Internal server error' },
53+
{ status: 500 },
54+
);
55+
}
56+
}

src/app/dashboard/page.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,15 @@ export default function DashboardPage() {
110110
</Link>
111111

112112
{/* 統計を見る */}
113-
<Card className="h-full relative bg-gray-100">
114-
<CardContent className="p-6 text-center">
115-
<div className="absolute top-2 right-2">
116-
<span className="px-2 py-1 bg-white text-gray-600 text-xs font-medium rounded-md border border-gray-300">
117-
Coming soon
118-
</span>
119-
</div>
120-
<BarChart3 className="w-8 h-8 text-gray-400 mx-auto mb-3" />
121-
<h3 className="font-semibold mb-2 text-gray-600">統計を見る</h3>
122-
<p className="text-sm text-gray-500">成長の記録を確認</p>
123-
</CardContent>
124-
</Card>
113+
<Link href="/analytics">
114+
<Card className="cursor-pointer hover:shadow-md transition-shadow h-full">
115+
<CardContent className="p-6 text-center">
116+
<BarChart3 className="w-8 h-8 text-purple-500 mx-auto mb-3" />
117+
<h3 className="font-semibold mb-2">統計を見る</h3>
118+
<p className="text-sm text-gray-600">成長の記録を確認</p>
119+
</CardContent>
120+
</Card>
121+
</Link>
125122

126123
{/* 設定 */}
127124
<Link href="/profile">

src/app/history/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export default function HistoryPage() {
157157
<SlideIn
158158
direction="bottom"
159159
duration={400}
160-
className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"
160+
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mb-8"
161161
>
162162
<div className="bg-white rounded-lg shadow-sm p-6">
163163
<p className="text-sm text-gray-600 mb-1">総振り返り数</p>
@@ -200,9 +200,9 @@ export default function HistoryPage() {
200200
</div>
201201
</FadeIn>
202202
) : (
203-
<FadeIn delay={100} duration={400} className="grid grid-cols-1 lg:grid-cols-3 gap-6">
203+
<FadeIn delay={100} duration={400} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
204204
{/* Calendar - Left side */}
205-
<div className="lg:col-span-1">
205+
<div className="md:col-span-1 lg:col-span-1">
206206
<Calendar
207207
reflections={reflections}
208208
frameworks={frameworks}
@@ -220,7 +220,7 @@ export default function HistoryPage() {
220220
}
221221
direction="right"
222222
duration={300}
223-
className="lg:col-span-2 bg-white rounded-lg shadow-sm overflow-hidden flex flex-col"
223+
className="md:col-span-1 lg:col-span-2 bg-white rounded-lg shadow-sm overflow-hidden flex flex-col"
224224
>
225225
{/* Panel Header */}
226226
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between flex-shrink-0">

0 commit comments

Comments
 (0)