Skip to content

Commit a995e8c

Browse files
committed
refactor: Atomic Designに基づきRecordingInterfaceコンポーネントを分割
- 単一責任の原則に従い、650行超のコンポーネントを複数の小さなコンポーネントに分割 - molecules層: RecordingHeader, RecordingTimer, RecordingControls, RecordingInitialState, RecordingInstructions, RecordingMiniDisplay, RecordingExpandedDisplay - organisms層: RecordingContainer, RecordingInterface - RefオブジェクトをMutableRefObjectに変更し型エラーを解消 - 各コンポーネントに適切なTSDocを追加
1 parent 16789fe commit a995e8c

19 files changed

Lines changed: 1233 additions & 803 deletions

File tree

Lines changed: 86 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
'use client'
1+
"use client";
22

3-
import { RecordButtonContent } from '@/components/molecules/RecordButtonContent'
4-
import type { RecordButtonProps } from './type'
3+
import { RecordButtonContent } from "@/components/molecules/RecordButtonContent";
4+
import type { RecordButtonProps } from "./type";
55

66
/**
77
* 録音ボタンコンポーネント
@@ -25,85 +25,95 @@ import type { RecordButtonProps } from './type'
2525
* ```
2626
*/
2727
export function RecordButton({
28-
status,
29-
onClick,
30-
disabled = false,
28+
status,
29+
onClick,
30+
disabled = false,
3131
}: RecordButtonProps) {
32-
// 状態に応じた背景グラデーションを取得
33-
const getBackgroundGradient = (): string => {
34-
switch (status) {
35-
case 'recording':
36-
return 'bg-gradient-to-br from-red-500 to-pink-500'
37-
case 'processing':
38-
return 'bg-gradient-to-br from-purple-500 to-indigo-500'
39-
case 'completed':
40-
return 'bg-gradient-to-br from-green-500 to-emerald-500'
41-
default:
42-
return 'bg-gradient-to-br from-purple-600 to-blue-600'
43-
}
44-
}
32+
// 状態に応じた背景グラデーションを取得
33+
const getBackgroundGradient = (): string => {
34+
switch (status) {
35+
case "recording":
36+
return "bg-gradient-to-br from-red-500 to-pink-500";
37+
case "processing":
38+
return "bg-gradient-to-br from-purple-500 to-indigo-500";
39+
case "completed":
40+
return "bg-gradient-to-br from-green-500 to-emerald-500";
41+
default:
42+
return "bg-gradient-to-br from-purple-600 to-blue-600";
43+
}
44+
};
4545

46-
// 状態に応じたアニメーションクラスを取得
47-
const getAnimationClass = (): string => {
48-
switch (status) {
49-
case 'recording':
50-
return 'animate-pulse-scale'
51-
case 'processing':
52-
return 'animate-spin-slow'
53-
case 'completed':
54-
return 'animate-bounce-once'
55-
default:
56-
return 'animate-float'
57-
}
58-
}
46+
// 状態に応じたアニメーションクラスを取得
47+
const getAnimationClass = (): string => {
48+
switch (status) {
49+
case "recording":
50+
return "animate-pulse-scale";
51+
case "processing":
52+
return "animate-spin-slow";
53+
case "completed":
54+
return "animate-bounce-once";
55+
default:
56+
return "animate-float";
57+
}
58+
};
5959

60-
// 状態に応じたリングエフェクトを取得
61-
const getRingEffect = (): string => {
62-
switch (status) {
63-
case 'recording':
64-
return 'ring-4 ring-red-400/30 ring-offset-4 ring-offset-transparent'
65-
case 'processing':
66-
return 'ring-4 ring-purple-400/30 ring-offset-4 ring-offset-transparent'
67-
default:
68-
return ''
69-
}
70-
}
60+
// 状態に応じたリングエフェクトを取得
61+
const getRingEffect = (): string => {
62+
switch (status) {
63+
case "recording":
64+
return "ring-4 ring-red-400/30 ring-offset-4 ring-offset-transparent";
65+
case "processing":
66+
return "ring-4 ring-purple-400/30 ring-offset-4 ring-offset-transparent";
67+
default:
68+
return "";
69+
}
70+
};
7171

72-
return (
73-
<div className="relative">
74-
{/* 背景の光彩エフェクト */}
75-
<div
76-
className={`absolute inset-0 ${getBackgroundGradient()} rounded-full opacity-50 blur-2xl ${getAnimationClass()}`}
77-
/>
72+
return (
73+
<div className="relative">
74+
{/* 背景の光彩エフェクト */}
75+
<div
76+
className={`absolute inset-0 ${getBackgroundGradient()} rounded-full blur-2xl opacity-50 ${getAnimationClass()}`}
77+
/>
7878

79-
{/* メインボタン */}
80-
<button
81-
type="button"
82-
onClick={onClick}
83-
disabled={disabled}
84-
className={`relative h-32 w-32 rounded-full ${getBackgroundGradient()}shadow-2xl transition-all duration-300 ease-out hover:scale-105 hover:shadow-3xl active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 ${getRingEffect()}
85-
${getAnimationClass()}group`}
86-
aria-label="録音"
87-
>
88-
{/* グラスエフェクト */}
89-
<div className="absolute inset-0 rounded-full bg-white/10 backdrop-blur-sm" />
79+
{/* メインボタン */}
80+
<button
81+
type="button"
82+
onClick={onClick}
83+
disabled={disabled}
84+
className={`
85+
relative w-32 h-32 rounded-full
86+
${getBackgroundGradient()}
87+
shadow-2xl
88+
transition-all duration-300 ease-out
89+
hover:scale-105 hover:shadow-3xl
90+
active:scale-95
91+
disabled:opacity-50 disabled:cursor-not-allowed
92+
${getRingEffect()}
93+
${getAnimationClass()}
94+
group
95+
`}
96+
aria-label="録音"
97+
>
98+
{/* グラスエフェクト */}
99+
<div className="absolute inset-0 rounded-full bg-white/10 backdrop-blur-sm" />
90100

91-
{/* 内部の光沢 */}
92-
<div className="absolute top-2 right-2 left-2 h-1/3 rounded-t-full bg-gradient-to-b from-white/30 to-transparent" />
101+
{/* 内部の光沢 */}
102+
<div className="absolute top-2 left-2 right-2 h-1/3 bg-gradient-to-b from-white/30 to-transparent rounded-t-full" />
93103

94-
{/* コンテンツ */}
95-
<div className="relative z-10">
96-
<RecordButtonContent status={status} />
97-
</div>
98-
</button>
104+
{/* コンテンツ */}
105+
<div className="relative z-10">
106+
<RecordButtonContent status={status} />
107+
</div>
108+
</button>
99109

100-
{/* 波紋エフェクト(録音中) */}
101-
{status === 'recording' && (
102-
<>
103-
<div className="absolute inset-0 animate-ping-slow rounded-full border-2 border-red-400/50" />
104-
<div className="absolute inset-0 animate-ping-slower rounded-full border-2 border-red-400/30" />
105-
</>
106-
)}
107-
</div>
108-
)
110+
{/* 波紋エフェクト(録音中) */}
111+
{status === "recording" && (
112+
<>
113+
<div className="absolute inset-0 rounded-full border-2 border-red-400/50 animate-ping-slow" />
114+
<div className="absolute inset-0 rounded-full border-2 border-red-400/30 animate-ping-slower" />
115+
</>
116+
)}
117+
</div>
118+
);
109119
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import { motion } from "framer-motion";
4+
import { RippleEffect } from "../../atoms/RippleEffect";
5+
import type { RecordingControlsProps } from "./types";
6+
7+
/**
8+
* 録音コントロールコンポーネント
9+
*
10+
* @description
11+
* 録音の一時停止・停止ボタンを提供するコンポーネント
12+
*
13+
* @param onStop 停止ボタンクリック時のコールバック
14+
* @param isRecording 録音中かどうか
15+
*/
16+
export function RecordingControls({
17+
onStop,
18+
isRecording,
19+
}: RecordingControlsProps) {
20+
return (
21+
<motion.div
22+
className="fixed left-0 right-0 bottom-20 sm:bottom-10 md:bottom-12 flex justify-center"
23+
initial={{ scale: 0 }}
24+
animate={{ scale: 1 }}
25+
transition={{
26+
delay: 0.3,
27+
type: "spring",
28+
stiffness: 200,
29+
}}
30+
>
31+
<motion.button
32+
onClick={onStop}
33+
className="relative w-20 h-20 sm:w-24 sm:h-24 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-all duration-300 shadow-lg touch-manipulation z-50"
34+
whileTap={{ scale: 0.95 }}
35+
>
36+
{/* 一時停止アイコン */}
37+
<div className="flex items-center gap-1.5">
38+
<div className="w-1 h-8 sm:h-10 bg-gray-900 rounded-full" />
39+
<div className="w-1 h-8 sm:h-10 bg-gray-900 rounded-full" />
40+
</div>
41+
42+
{/* リップルエフェクト */}
43+
<RippleEffect isActive={isRecording} borderColor="border-gray-400" />
44+
</motion.button>
45+
</motion.div>
46+
);
47+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* RecordingControlsコンポーネントのプロパティ型定義
3+
*/
4+
export interface RecordingControlsProps {
5+
/**
6+
* 停止ボタンクリック時のコールバック
7+
*/
8+
onStop: () => void;
9+
10+
/**
11+
* 録音中かどうか
12+
*/
13+
isRecording: boolean;
14+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
import { motion } from "framer-motion";
4+
import { RecordingControls } from "../RecordingControls";
5+
import { RecordingHeader } from "../RecordingHeader";
6+
import { RecordingTimer } from "../RecordingTimer";
7+
import { WaveformDisplay } from "../WaveformDisplay";
8+
import type { RecordingExpandedDisplayProps } from "./types";
9+
10+
/**
11+
* 録音拡大表示コンポーネント
12+
*
13+
* @description
14+
* 録音インターフェースの展開時に表示されるコンポーネント
15+
*
16+
* @param status 録音状態
17+
* @param recordingTime 録音時間
18+
* @param waveformData 波形データ
19+
* @param formatTime 時間フォーマット関数
20+
* @param onCancel キャンセルボタンクリック時のコールバック
21+
* @param onNext 次へボタンクリック時のコールバック
22+
* @param onStop 停止ボタンクリック時のコールバック
23+
*/
24+
export function RecordingExpandedDisplay({
25+
status,
26+
recordingTime,
27+
waveformData,
28+
formatTime,
29+
onCancel,
30+
onNext,
31+
onStop,
32+
}: RecordingExpandedDisplayProps) {
33+
return (
34+
<motion.div
35+
initial={{ opacity: 0 }}
36+
animate={{ opacity: 1 }}
37+
exit={{ opacity: 0 }}
38+
transition={{ duration: 0.3 }}
39+
className="flex-1 flex flex-col"
40+
>
41+
{/* ヘッダー部分 */}
42+
<RecordingHeader
43+
isRecording={status === "recording"}
44+
onCancel={onCancel}
45+
onNext={onNext}
46+
/>
47+
48+
{/* メインコンテンツエリア */}
49+
<div className="flex-1 flex flex-col items-center justify-start pt-4 px-6 sm:px-8">
50+
{/* タイマー表示 */}
51+
<RecordingTimer time={recordingTime} formatTime={formatTime} />
52+
53+
{/* 波形表示 */}
54+
<motion.div
55+
initial={{ scale: 0.9, opacity: 0 }}
56+
animate={{ scale: 1, opacity: 1 }}
57+
transition={{ delay: 0.2 }}
58+
className="w-full max-w-2xl px-4 mb-4"
59+
>
60+
<WaveformDisplay
61+
isRecording={status === "recording"}
62+
isCompleted={status === "completed"}
63+
recordingTime={recordingTime}
64+
waveformData={waveformData}
65+
height={160}
66+
className="h-32 sm:h-40"
67+
waveColor="#000000"
68+
backgroundColor="#f3f4f6"
69+
key={`expanded-${status}-${recordingTime}`}
70+
/>
71+
</motion.div>
72+
</div>
73+
74+
{/* 停止ボタン */}
75+
<RecordingControls onStop={onStop} isRecording={status === "recording"} />
76+
</motion.div>
77+
);
78+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* RecordingExpandedDisplayコンポーネントのプロパティ型定義
3+
*/
4+
export interface RecordingExpandedDisplayProps {
5+
/**
6+
* 録音状態
7+
*/
8+
status: "idle" | "recording" | "completed";
9+
10+
/**
11+
* 録音時間
12+
*/
13+
recordingTime: number;
14+
15+
/**
16+
* 波形データ
17+
*/
18+
waveformData: number[];
19+
20+
/**
21+
* 時間フォーマット関数
22+
* @param time - 録音時間(秒)
23+
* @returns フォーマットされた時間文字列
24+
*/
25+
formatTime: (time: number) => string;
26+
27+
/**
28+
* キャンセルボタンクリック時のコールバック
29+
*/
30+
onCancel: () => void;
31+
32+
/**
33+
* 次へボタンクリック時のコールバック
34+
*/
35+
onNext: () => void;
36+
37+
/**
38+
* 停止ボタンクリック時のコールバック
39+
*/
40+
onStop: () => void;
41+
}

0 commit comments

Comments
 (0)