| | 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); |