Skip to content

Commit 0934502

Browse files
authored
Merge pull request #42 from kc3hack/feature/sound
feat: 全画面へのBGM・SE実装およびトップページのUI修復完了
2 parents f1f441f + ff38d35 commit 0934502

14 files changed

Lines changed: 365 additions & 61 deletions

File tree

28.7 KB
Binary file not shown.
1.83 MB
Binary file not shown.
2.16 MB
Binary file not shown.
21.4 KB
Binary file not shown.
825 KB
Binary file not shown.
4.29 MB
Binary file not shown.

frontend/src/app/page.tsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import Image from 'next/image';
44
import { useRouter } from 'next/navigation';
5-
import { useState } from 'react';
5+
import { useEffect, useRef, useState } from 'react';
66
import type { CSSProperties } from 'react';
77

8-
//まる爆発アニメーションコンポーネント
8+
// --- まる爆発アニメーションコンポーネント ---
99
const SparklesExplosion = () => {
1010
const count = 20;
1111
const colors = ['#e9eb7c', '#ee7ee6', '#6eb8ca', '#e17a78', '#91ec77'];
@@ -56,16 +56,51 @@ const SparklesExplosion = () => {
5656
);
5757
};
5858

59+
// --- メインのトップページコンポーネント ---
5960
export default function TopPage() {
6061
const router = useRouter();
6162
const [showExplosion, setShowExplosion] = useState(false);
6263

64+
// BGMを保持するための Ref
65+
const bgmRef = useRef<HTMLAudioElement | null>(null);
66+
67+
// BGMの初期化と再生管理
68+
useEffect(() => {
69+
// パスは public/sounds/start-bgm.mp3 を想定
70+
const bgm = new Audio('/sounds/start-bgm.mp3');
71+
bgm.loop = true;
72+
bgm.volume = 0.4;
73+
bgmRef.current = bgm;
74+
75+
const playBGM = () => {
76+
bgm.play().catch(() => {
77+
// 自動再生制限がかかった場合は何もしない
78+
});
79+
// 一度クリックされたらイベントリスナーを削除
80+
window.removeEventListener('click', playBGM);
81+
};
82+
83+
window.addEventListener('click', playBGM);
84+
85+
// クリーンアップ
86+
return () => {
87+
bgm.pause();
88+
window.removeEventListener('click', playBGM);
89+
};
90+
}, []);
91+
6392
const handleStartClick = () => {
6493
setShowExplosion(true);
6594

95+
// SEの再生(パスを修正)
6696
const audio = new Audio('/sounds/start-se.mp3');
6797
audio.play().catch(() => {});
6898

99+
// ボタン押下時にBGMを停止
100+
if (bgmRef.current) {
101+
bgmRef.current.pause();
102+
}
103+
69104
setTimeout(() => {
70105
router.push('/games/terms');
71106
}, 800);
@@ -76,7 +111,18 @@ export default function TopPage() {
76111
};
77112

78113
return (
79-
<div className="flex h-dvh w-full flex-col items-center justify-center overflow-hidden bg-top-pattern">
114+
<div
115+
className="flex h-dvh w-full flex-col items-center justify-center overflow-hidden bg-top-pattern"
116+
style={{
117+
backgroundImage: `
118+
radial-gradient(circle, rgba(255,255,255,0.8) 1.0px, transparent 4px),
119+
url('/images/bg-pattern.svg')
120+
`,
121+
backgroundSize: '16px 16px, cover',
122+
backgroundPosition: '0 0, center',
123+
backgroundRepeat: 'repeat, no-repeat',
124+
}}
125+
>
80126
<div className="flex flex-col items-center gap-[2vh] w-full">
81127
<Image
82128
src="/images/RealYouLogo.png"

frontend/src/features/diagnosis/components/BaselineSurvey.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useCallback } from 'react';
3+
import { useState, useCallback, useEffect, useRef } from 'react';
44
import { useAtomValue } from 'jotai';
55
import { useRouter } from 'next/navigation';
66
import { mbtiAtom } from '@/stores/diagnosis';
@@ -22,6 +22,35 @@ export default function BaselineSurvey() {
2222
const mbti = useAtomValue(mbtiAtom);
2323
const game1Data = useAtomValue(game1DataAtom);
2424

25+
const bgmRef = useRef<HTMLAudioElement | null>(null);
26+
27+
const playSE = useCallback((path: string) => {
28+
const audio = new Audio(path);
29+
audio.volume = 0.5;
30+
audio.play().catch(() => {});
31+
}, []);
32+
33+
// BGMの初期化と再生管理
34+
useEffect(() => {
35+
const bgm = new Audio('/sounds/start-bgm.mp3');
36+
bgm.loop = true;
37+
bgm.volume = 0.4;
38+
bgmRef.current = bgm;
39+
40+
const playBGM = () => {
41+
bgm.play().catch(() => {});
42+
window.removeEventListener('click', playBGM);
43+
};
44+
45+
window.addEventListener('click', playBGM);
46+
playBGM();
47+
48+
return () => {
49+
bgm.pause();
50+
window.removeEventListener('click', playBGM);
51+
};
52+
}, []);
53+
2554
const [currentIndex, setCurrentIndex] = useState(0);
2655
const [answers, setAnswers] = useState<
2756
Partial<Record<QuestionKey, AnswerOption>>
@@ -66,6 +95,7 @@ export default function BaselineSurvey() {
6695
const handleAnswer = (value: AnswerOption) => {
6796
const newAnswers = { ...answers, [currentQuestion.key]: value };
6897
setAnswers(newAnswers);
98+
playSE('/sounds/general-button-se.mp3');
6999

70100
if (currentIndex < totalQuestions - 1) {
71101
setCurrentIndex((prev) => prev + 1);
@@ -75,6 +105,7 @@ export default function BaselineSurvey() {
75105
};
76106

77107
const handleRetry = () => {
108+
playSE('/sounds/general-button-se.mp3');
78109
submitToApi(answers as BaselineAnswers);
79110
};
80111

frontend/src/features/diagnosis/components/MbtiSelect.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import Image from 'next/image';
4-
import { useState, useCallback } from 'react';
4+
import { useState, useCallback, useEffect, useRef } from 'react';
55
import { flushSync } from 'react-dom';
66
import { motion, AnimatePresence } from 'framer-motion';
77
import { useSetAtom } from 'jotai';
@@ -39,6 +39,36 @@ export default function MbtiSelect() {
3939
const [transitionVia, setTransitionVia] = useState<
4040
'tab' | 'arrow-left' | 'arrow-right'
4141
>('tab');
42+
const bgmRef = useRef<HTMLAudioElement | null>(null);
43+
44+
const playSE = useCallback((path: string) => {
45+
const audio = new Audio(path);
46+
audio.volume = 0.5;
47+
audio.play().catch(() => {});
48+
}, []);
49+
50+
// BGMの初期化と再生管理
51+
useEffect(() => {
52+
// TopPageと同じ start-bgm を使用
53+
const bgm = new Audio('/sounds/start-bgm.mp3');
54+
bgm.loop = true;
55+
bgm.volume = 0.4;
56+
bgmRef.current = bgm;
57+
58+
const playBGM = () => {
59+
bgm.play().catch(() => {});
60+
window.removeEventListener('click', playBGM);
61+
};
62+
63+
window.addEventListener('click', playBGM);
64+
// すでに他のページでインタラクションがあれば即再生される
65+
playBGM();
66+
67+
return () => {
68+
bgm.pause();
69+
window.removeEventListener('click', playBGM);
70+
};
71+
}, []);
4272

4373
const currentGroup = MBTI_GROUPS[groupIndex];
4474
const currentTypes = getTypesByGroup(currentGroup);
@@ -54,33 +84,39 @@ export default function MbtiSelect() {
5484
flushSync(() => setTransitionVia('tab'));
5585
setGroupIndex(index);
5686
setSelected(null);
87+
playSE('/sounds/general-button-se.mp3');
5788
},
58-
[groupIndex]
89+
[groupIndex, playSE]
5990
);
6091

6192
const handleArrowPrev = useCallback(() => {
6293
setTransitionVia('arrow-right'); // コンテンツは右から入る
6394
setGroupIndex((i) => (i - 1 + MBTI_GROUPS.length) % MBTI_GROUPS.length);
6495
setSelected(null);
65-
}, []);
96+
playSE('/sounds/general-button-se.mp3');
97+
}, [playSE]);
6698

6799
const handleArrowNext = useCallback(() => {
68100
setTransitionVia('arrow-left'); // コンテンツは左から入る
69101
setGroupIndex((i) => (i + 1) % MBTI_GROUPS.length);
70102
setSelected(null);
71-
}, []);
103+
playSE('/sounds/general-button-se.mp3');
104+
}, [playSE]);
72105

73106
const handleConfirm = () => {
107+
playSE('/sounds/general-button-se.mp3');
74108
if (!selected) return;
75109
setMbti(selected);
76110
setStep('quiz');
77111
};
78112

79113
const handleReselect = () => {
114+
playSE('/sounds/general-button-se.mp3');
80115
setSelected(null);
81116
};
82117

83118
const handleSkip = () => {
119+
playSE('/sounds/general-button-se.mp3');
84120
setMbti(null);
85121
setStep('quiz');
86122
};
@@ -184,7 +220,10 @@ export default function MbtiSelect() {
184220
<motion.button
185221
key={type.code}
186222
type="button"
187-
onClick={() => setSelected(type.code)}
223+
onClick={() => {
224+
setSelected(type.code);
225+
playSE('/sounds/general-button-se.mp3');
226+
}}
188227
className="relative z-0 flex flex-1 basis-0 flex-col cursor-pointer items-center justify-center overflow-hidden rounded-4xl border-2 border-gray-800 bg-white/80 outline-none ring-0 hover:z-50"
189228
whileHover={{
190229
scale: 1.2,

frontend/src/features/games/group-chat/components/GroupChatGameFlow.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,36 @@ export default function GroupChatGameFlow() {
2424
const chatEndRef = useRef<HTMLDivElement>(null);
2525
const [submitStatus, setSubmitStatus] = useState<SubmitStatus>('loading');
2626
const pendingDataRef = useRef<Game3Data | null>(null);
27+
const bgmRef = useRef<HTMLAudioElement | null>(null);
28+
29+
const playSE = useCallback((path: string) => {
30+
const audio = new Audio(path);
31+
audio.volume = 0.5;
32+
audio.play().catch(() => {});
33+
}, []);
34+
35+
// BGMの初期化と再生管理
36+
useEffect(() => {
37+
// 指示はgame2でしたが、Game3の画面のためgame3-bgm.mp3を適用します
38+
const bgm = new Audio('/sounds/game3-bgm.mp3');
39+
bgm.loop = true;
40+
bgm.volume = 0.3;
41+
bgmRef.current = bgm;
42+
43+
const playBGM = () => {
44+
bgm.play().catch(() => {});
45+
window.removeEventListener('click', playBGM);
46+
};
47+
48+
window.addEventListener('click', playBGM);
49+
// 前の画面から継続している場合は即再生
50+
playBGM();
51+
52+
return () => {
53+
bgm.pause();
54+
window.removeEventListener('click', playBGM);
55+
};
56+
}, []);
2757

2858
const submitGame3 = useCallback(async (data: Game3Data) => {
2959
const userId = localStorage.getItem('user_id');
@@ -43,6 +73,7 @@ export default function GroupChatGameFlow() {
4373
try {
4474
await submitGame3(data);
4575
setSubmitStatus('success');
76+
bgmRef.current?.pause();
4677
setTimeout(() => {
4778
router.push('/result');
4879
}, 2000);
@@ -54,19 +85,21 @@ export default function GroupChatGameFlow() {
5485
);
5586

5687
const handleRetry = useCallback(async () => {
88+
playSE('/sounds/general-button-se.mp3'); // リトライ音
5789
const data = pendingDataRef.current;
5890
if (!data) return;
5991
setSubmitStatus('loading');
6092
try {
6193
await submitGame3(data);
6294
setSubmitStatus('success');
95+
bgmRef.current?.pause();
6396
setTimeout(() => {
6497
router.push('/result');
6598
}, 2000);
6699
} catch {
67100
setSubmitStatus('error');
68101
}
69-
}, [router, submitGame3]);
102+
}, [router, submitGame3, playSE]);
70103

71104
const {
72105
gamePhase,
@@ -76,14 +109,28 @@ export default function GroupChatGameFlow() {
76109
remainingTimeMs,
77110
isTypingIndicatorVisible,
78111
typingBotName,
79-
startGame,
80-
selectOption,
112+
startGame: originalStartGame, // 名前を変更してラップ
113+
selectOption: originalSelectOption, // 名前を変更してラップ
81114
handleOptionHover,
82115
stageTimeLimitMs,
83116
groupName,
84117
groupMemberCount,
85118
} = useGroupChatGame({ onComplete: handleComplete });
86119

120+
// サウンドを鳴らすようにラップ
121+
const startGame = useCallback(() => {
122+
playSE('/sounds/general-button-se.mp3');
123+
originalStartGame();
124+
}, [originalStartGame, playSE]);
125+
126+
const selectOption = useCallback(
127+
(optionId: number) => {
128+
playSE('/sounds/general-button-se.mp3');
129+
originalSelectOption(optionId);
130+
},
131+
[originalSelectOption, playSE]
132+
);
133+
87134
useEffect(() => {
88135
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
89136
}, [chatMessages, isTypingIndicatorVisible, gamePhase]);

0 commit comments

Comments
 (0)