diff --git a/components/JournalCard.tsx b/components/JournalCard.tsx index bce726e..501bbda 100644 --- a/components/JournalCard.tsx +++ b/components/JournalCard.tsx @@ -32,6 +32,24 @@ export const JournalCard: React.FC = ({ const [isPlayingAudio, setIsPlayingAudio] = useState(false); const [stampFeedback, setStampFeedback] = useState(false); + const audioRef = React.useRef(null); + const audioContextRef = React.useRef(null); + const synthTimerRef = React.useRef(null); + + React.useEffect(() => { + return () => { + if (audioRef.current) { + audioRef.current.pause(); + } + if (synthTimerRef.current) { + clearInterval(synthTimerRef.current); + } + if (audioContextRef.current) { + audioContextRef.current.close().catch(() => {}); + } + }; + }, []); + const handleLike = (e: React.MouseEvent) => { e.stopPropagation(); if (!hasLiked) { @@ -47,7 +65,77 @@ export const JournalCard: React.FC = ({ const toggleAudio = (e: React.MouseEvent) => { e.stopPropagation(); - setIsPlayingAudio(!isPlayingAudio); + if (isPlayingAudio) { + stopAudio(); + } else { + startAudio(); + } + }; + + const stopAudio = () => { + if (audioRef.current) { + audioRef.current.pause(); + } + if (synthTimerRef.current) { + clearInterval(synthTimerRef.current); + synthTimerRef.current = null; + } + if (audioContextRef.current) { + audioContextRef.current.close().catch(() => {}); + audioContextRef.current = null; + } + setIsPlayingAudio(false); + }; + + const startAudio = () => { + if (post.audioUrl) { + if (!audioRef.current) { + audioRef.current = new Audio(post.audioUrl); + audioRef.current.onended = () => setIsPlayingAudio(false); + audioRef.current.onpause = () => setIsPlayingAudio(false); + } + audioRef.current.play() + .then(() => setIsPlayingAudio(true)) + .catch(() => playSynthFallback()); + } else { + playSynthFallback(); + } + }; + + const playSynthFallback = () => { + try { + const AudioCtx = window.AudioContext || (window as any).webkitAudioContext; + if (!AudioCtx) { + setIsPlayingAudio(true); + return; + } + const ctx = new AudioCtx(); + audioContextRef.current = ctx; + setIsPlayingAudio(true); + + const notes = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25]; + let noteIdx = 0; + + const playNextNote = () => { + if (!audioContextRef.current || ctx.state === "closed") return; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = "sine"; + osc.frequency.setValueAtTime(notes[noteIdx % notes.length], ctx.currentTime); + gain.gain.setValueAtTime(0.06, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5); + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(); + osc.stop(ctx.currentTime + 0.5); + noteIdx++; + }; + + playNextNote(); + synthTimerRef.current = setInterval(playNextNote, 450); + } catch (e) { + setIsPlayingAudio(true); + } }; // Render format based on type diff --git a/components/types.ts b/components/types.ts index 97aff13..1aaa0a8 100644 --- a/components/types.ts +++ b/components/types.ts @@ -34,5 +34,6 @@ export interface JournalPost { codeLanguage?: string; audioDuration?: string; audioTitle?: string; + audioUrl?: string; stampedText?: string; } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 98fddab..77d67e5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,6 +47,7 @@ model Post { codeLanguage String? @map("code_language") audioDuration String? @map("audio_duration") audioTitle String? @map("audio_title") + audioUrl String? @map("audio_url") stampedText String? @map("stamped_text") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/prisma/schema.sql b/prisma/schema.sql index 5d66ae4..e1c786b 100644 --- a/prisma/schema.sql +++ b/prisma/schema.sql @@ -29,9 +29,10 @@ CREATE TABLE IF NOT EXISTS posts ( image_caption TEXT, code_snippet TEXT, code_language VARCHAR(50), - audio_duration VARCHAR(50), - audio_title TEXT, - stamped_text VARCHAR(100), + audio_duration VARCHAR(20), + audio_title VARCHAR(255), + audio_url TEXT, + stamped_text VARCHAR(50), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL );