|
|
class SelfTalkRecorder extends HTMLElement { |
|
|
connectedCallback() { |
|
|
this.attachShadow({ mode: 'open' }); |
|
|
this.shadowRoot.innerHTML = ` |
|
|
<style> |
|
|
:host { |
|
|
display: block; |
|
|
} |
|
|
.container { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 1.5rem; |
|
|
} |
|
|
.video-preview { |
|
|
width: 100%; |
|
|
height: 300px; |
|
|
background: #1e293b; |
|
|
border-radius: 0.5rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
video { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: cover; |
|
|
} |
|
|
.placeholder { |
|
|
text-align: center; |
|
|
color: #64748b; |
|
|
} |
|
|
.controls { |
|
|
display: flex; |
|
|
gap: 1rem; |
|
|
} |
|
|
button { |
|
|
flex: 1; |
|
|
padding: 0.75rem; |
|
|
border-radius: 0.5rem; |
|
|
font-weight: 500; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 0.5rem; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.record-btn { |
|
|
background: #7c3aed; |
|
|
color: white; |
|
|
border: none; |
|
|
} |
|
|
.record-btn:hover { |
|
|
background: #6d28d9; |
|
|
} |
|
|
.record-btn.recording { |
|
|
background: #dc2626; |
|
|
animation: pulse 1.5s infinite; |
|
|
} |
|
|
.stop-btn { |
|
|
background: #1e293b; |
|
|
color: white; |
|
|
border: 1px solid #334155; |
|
|
} |
|
|
.stop-btn:hover { |
|
|
background: #334155; |
|
|
} |
|
|
.timer { |
|
|
font-size: 1.25rem; |
|
|
font-weight: 600; |
|
|
color: #7c3aed; |
|
|
text-align: center; |
|
|
} |
|
|
@keyframes pulse { |
|
|
0% { opacity: 1; } |
|
|
50% { opacity: 0.7; } |
|
|
100% { opacity: 1; } |
|
|
} |
|
|
</style> |
|
|
<div class="container"> |
|
|
<h2 class="text-xl font-bold gradient-text mb-2">Record Your Self-Talk</h2> |
|
|
<p class="text-slate-300 mb-4">Speak freely for 1 minute about your thoughts, feelings, or affirmations.</p> |
|
|
|
|
|
<div class="video-preview"> |
|
|
<video id="videoPreview" autoplay muted></video> |
|
|
<div class="placeholder" id="videoPlaceholder"> |
|
|
<i data-feather="video" class="w-12 h-12 mx-auto mb-2"></i> |
|
|
<p>Video preview will appear here</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="timer" id="timer">01:00</div> |
|
|
|
|
|
<div class="controls"> |
|
|
<button class="record-btn" id="recordBtn"> |
|
|
<i data-feather="mic"></i> Start Recording |
|
|
</button> |
|
|
<button class="stop-btn" id="stopBtn" disabled> |
|
|
<i data-feather="square"></i> Stop |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
this.mediaRecorder = null; |
|
|
this.recordedChunks = []; |
|
|
this.countdownInterval = null; |
|
|
this.timeLeft = 60; |
|
|
|
|
|
this.setupEventListeners(); |
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
const recordBtn = this.shadowRoot.getElementById('recordBtn'); |
|
|
const stopBtn = this.shadowRoot.getElementById('stopBtn'); |
|
|
const videoPreview = this.shadowRoot.getElementById('videoPreview'); |
|
|
const videoPlaceholder = this.shadowRoot.getElementById('videoPlaceholder'); |
|
|
const timer = this.shadowRoot.getElementById('timer'); |
|
|
|
|
|
recordBtn.addEventListener('click', async () => { |
|
|
try { |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
video: true, |
|
|
audio: true |
|
|
}); |
|
|
|
|
|
videoPreview.srcObject = stream; |
|
|
videoPlaceholder.style.display = 'none'; |
|
|
videoPreview.style.display = 'block'; |
|
|
|
|
|
this.mediaRecorder = new MediaRecorder(stream); |
|
|
this.recordedChunks = []; |
|
|
|
|
|
this.mediaRecorder.ondataavailable = event => { |
|
|
if (event.data.size > 0) { |
|
|
this.recordedChunks.push(event.data); |
|
|
} |
|
|
}; |
|
|
|
|
|
this.mediaRecorder.onstop = () => { |
|
|
const blob = new Blob(this.recordedChunks, { type: 'video/webm' }); |
|
|
this.saveRecording(blob); |
|
|
}; |
|
|
|
|
|
this.mediaRecorder.start(100); |
|
|
recordBtn.classList.add('recording'); |
|
|
recordBtn.disabled = true; |
|
|
stopBtn.disabled = false; |
|
|
|
|
|
|
|
|
this.timeLeft = 60; |
|
|
this.countdownInterval = setInterval(() => { |
|
|
this.timeLeft--; |
|
|
const minutes = Math.floor(this.timeLeft / 60); |
|
|
const seconds = this.timeLeft % 60; |
|
|
timer.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
|
|
|
|
|
if (this.timeLeft <= 0) { |
|
|
this.stopRecording(); |
|
|
} |
|
|
}, 1000); |
|
|
} catch (error) { |
|
|
console.error('Error accessing media devices:', error); |
|
|
alert('Could not access camera/microphone. Please check permissions.'); |
|
|
} |
|
|
}); |
|
|
|
|
|
stopBtn.addEventListener('click', () => this.stopRecording()); |
|
|
} |
|
|
|
|
|
stopRecording() { |
|
|
if (this.countdownInterval) { |
|
|
clearInterval(this.countdownInterval); |
|
|
this.countdownInterval = null; |
|
|
} |
|
|
|
|
|
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') { |
|
|
this.mediaRecorder.stop(); |
|
|
|
|
|
const videoPreview = this.shadowRoot.getElementById('videoPreview'); |
|
|
const stream = videoPreview.srcObject; |
|
|
stream.getTracks().forEach(track => track.stop()); |
|
|
|
|
|
this.shadowRoot.getElementById('recordBtn').classList.remove('recording'); |
|
|
this.shadowRoot.getElementById('recordBtn').disabled = false; |
|
|
this.shadowRoot.getElementById('stopBtn').disabled = true; |
|
|
this.shadowRoot.getElementById('timer').textContent = '01:00'; |
|
|
} |
|
|
} |
|
|
|
|
|
async saveRecording(blob) { |
|
|
|
|
|
console.log('Recording saved:', blob); |
|
|
alert('Recording saved successfully! It will appear in your journal entries.'); |
|
|
|
|
|
|
|
|
const entriesContainer = document.getElementById('entriesContainer'); |
|
|
if (entriesContainer) { |
|
|
const newEntry = { |
|
|
id: Date.now().toString(), |
|
|
date: new Date().toISOString().split('T')[0], |
|
|
duration: '1:00', |
|
|
thumbnail: 'http://static.photos/people/320x240/' + Math.floor(Math.random() * 10), |
|
|
mood: 'New' |
|
|
}; |
|
|
|
|
|
entriesContainer.insertAdjacentHTML('afterbegin', ` |
|
|
<div class="glass-card entry-card p-6 cursor-pointer"> |
|
|
<div class="relative pb-[56.25%] mb-4 overflow-hidden rounded-lg"> |
|
|
<img src="${newEntry.thumbnail}" alt="Entry thumbnail" class="absolute h-full w-full object-cover"> |
|
|
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-3"> |
|
|
<div class="text-white font-medium">${newEntry.duration}</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex justify-between items-center"> |
|
|
<div> |
|
|
<h3 class="font-bold">${newEntry.date}</h3> |
|
|
<p class="text-sm text-slate-400">${newEntry.mood}</p> |
|
|
</div> |
|
|
<button class="text-indigo-400 hover:text-indigo-300"> |
|
|
<i data-feather="play"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`); |
|
|
|
|
|
feather.replace(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
customElements.define('self-talk-recorder', SelfTalkRecorder); |