(function() { 'use strict'; class LocutoresWidget extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.voices = []; this.categories = []; this.selectedCategory = 'all'; this.playingId = null; this.visibleCount = 48; this.audioElements = {}; this.loadError = false; this.baseUrl = 'https://sistema.offsbarato.com.br'; } connectedCallback() { this.render(); this.loadData(); this.addStyles(); } addStyles() { const style = document.createElement('style'); style.textContent = ` :host { display: block; width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .container { width: 100%; padding: 64px 0; } .filters { margin-bottom: 32px; max-width: 1152px; margin-left: auto; margin-right: auto; padding: 0 24px; } .filter-buttons { display: flex; flex-wrap: wrap; gap: 12px; } .filter-btn { padding: 12px 24px; border-radius: 12px; font-weight: 500; transition: all 0.3s; cursor: pointer; border: 2px solid #fef3c7; background: white; color: #374151; } .filter-btn:hover { border-color: #fcd34d; } .filter-btn.active { background: linear-gradient(to right, #f59e0b, #d97706); color: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); } .filter-btn.duty-btn { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; display: flex; align-items: center; gap: 8px; } .filter-btn.duty-btn:hover { border-color: #86efac; } .filter-btn.duty-btn.active { background: linear-gradient(to right, #16a34a, #15803d); color: white; } .duty-dot { width: 12px; height: 12px; border-radius: 50%; background: #10b981; position: relative; display: inline-block; } .duty-dot::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: #10b981; animation: pulse-dot 2s ease-in-out infinite; } @keyframes pulse-dot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.2); } } .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; width: 100%; } @media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } } @media (min-width: 1024px) { .grid { grid-template-columns: repeat(6, 1fr); } } .voice-card { position: relative; aspect-ratio: 4/4.5; overflow: hidden; cursor: pointer; background-size: cover; background-position: center; } .voice-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0.6), transparent); transition: all 0.3s; } .voice-card:hover .voice-overlay { background: linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0.6), transparent); } .voice-content { position: absolute; inset: 0; padding: 16px; display: flex; flex-direction: column; justify-content: space-between; z-index: 10; } .voice-header { display: flex; justify-content: space-between; align-items: start; } .play-btn { width: 56px; height: 56px; border-radius: 50%; background: #facc15; display: flex; align-items: center; justify-content: center; color: black; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); transition: all 0.3s; border: none; cursor: pointer; } .play-btn svg { width: 28px; height: 28px; } .play-btn:hover { background: #eab308; transform: scale(1.1); } .badge { background: rgba(245, 158, 11, 0.9); color: white; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; } .voice-footer { display: flex; flex-direction: column; gap: 8px; } .voice-name { font-weight: bold; color: white; font-size: 14px; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .voice-status { display: flex; align-items: center; gap: 4px; font-weight: 500; font-size: 12px; } .status-dot { width: 6px; height: 6px; border-radius: 50%; animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .order-btn { width: 100%; background: linear-gradient(to right, #16a34a, #15803d); color: white; font-size: 12px; padding: 8px; border-radius: 6px; border: none; cursor: pointer; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.3s; line-height: 1.4; } .order-btn svg { width: 12px; height: 12px; flex-shrink: 0; } .order-btn:hover { background: linear-gradient(to right, #15803d, #166534); } .loading { display: flex; justify-content: center; padding: 80px 0; } .spinner { width: 64px; height: 64px; border: 2px solid #f59e0b; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .load-more { display: flex; justify-content: center; margin-top: 32px; max-width: 1152px; margin-left: auto; margin-right: auto; padding: 0 24px; } .load-more-btn { background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(4px); border: 2px solid #f59e0b; color: #78350f; padding: 24px 32px; font-size: 18px; border-radius: 12px; cursor: pointer; font-weight: 600; transition: all 0.3s; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); } .load-more-btn:hover { background: #f59e0b; color: white; } `; this.shadowRoot.appendChild(style); } async loadData() { try { const scriptElement = document.querySelector('script[src*="serveWidget"]'); if (scriptElement && scriptElement.src) { const scriptUrl = new URL(scriptElement.src); this.baseUrl = scriptUrl.origin; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const [voicesRes, categoriesRes] = await Promise.all([ fetch(`${this.baseUrl}/functions/getWidgetVoices`, { signal: controller.signal }), fetch(`${this.baseUrl}/functions/getWidgetCategories`, { signal: controller.signal }) ]); clearTimeout(timeoutId); if (!voicesRes.ok || !categoriesRes.ok) { throw new Error(`Erro ao carregar dados`); } const voicesData = await voicesRes.json(); const categoriesData = await categoriesRes.json(); // Smart Shuffle const grouped = {}; voicesData.forEach(voice => { if (!grouped[voice.name]) grouped[voice.name] = []; grouped[voice.name].push(voice); }); const shuffledVoices = []; const names = Object.keys(grouped).sort(() => Math.random() - 0.5); let maxCount = 0; Object.values(grouped).forEach(g => maxCount = Math.max(maxCount, g.length)); for (let i = 0; i < maxCount; i++) { names.forEach(name => { if (grouped[name][i]) { shuffledVoices.push(grouped[name][i]); } }); } // Ordenar: locutores gravando primeiro const now = new Date(); const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); const today = now.toISOString().split('T')[0]; const recordingUserIds = new Set(); const recordingVoiceIds = new Set(); shuffledVoices.forEach(voice => { if (this.isVoiceRecording(voice, now, currentTime, today, shuffledVoices)) { if (voice.user_id) { recordingUserIds.add(voice.user_id); } else { recordingVoiceIds.add(voice.id); } } }); const sorted = shuffledVoices.sort((a, b) => { const isARecording = (a.user_id && recordingUserIds.has(a.user_id)) || recordingVoiceIds.has(a.id); const isBRecording = (b.user_id && recordingUserIds.has(b.user_id)) || recordingVoiceIds.has(b.id); if (isARecording && !isBRecording) return -1; if (!isARecording && isBRecording) return 1; return 0; }); this.voices = sorted; this.categories = categoriesData; this.render(); } catch (error) { console.error('Widget: Erro ao carregar dados:', error); this.voices = []; this.categories = []; this.loadError = true; this.render(); } } isVoiceRecording(voice, now, currentTime, today, allVoices) { const voicesToCheck = voice.user_id ? allVoices.filter(v => v.user_id === voice.user_id) : [voice]; for (const v of voicesToCheck) { if (v.on_duty && v.duty_date === today && v.duty_end_time >= currentTime) { return true; } if (v.recording_schedule) { const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayKey = days[now.getDay()]; const sched = v.recording_schedule[dayKey]; if (sched && sched.works && currentTime >= sched.start && currentTime <= sched.end) { return true; } } } return false; } getAvailabilityInfo(voice) { const now = new Date(); const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); const todayDate = now.toISOString().split('T')[0]; const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const currentDayKey = daysOfWeek[now.getDay()]; // Manual On Duty const isManualDuty = voice.on_duty && voice.duty_date === todayDate && voice.duty_end_time >= currentTime; if (isManualDuty) { return { text: `Locutor de Plantão HOJE até as ${voice.duty_end_time}`, color: "#a7f3d0", dotColor: "#10b981" }; } // Check Schedule let schedule = voice.recording_schedule; if (!schedule && voice.user_id) { const sameUser = this.voices.find(v => v.user_id === voice.user_id && v.recording_schedule); if (sameUser) schedule = sameUser.recording_schedule; } if (schedule) { const todaySchedule = schedule[currentDayKey]; if (todaySchedule && todaySchedule.works && currentTime >= todaySchedule.start && currentTime <= todaySchedule.end) { return { text: `Gravando até as ${todaySchedule.end}`, color: "#a7f3d0", dotColor: "#10b981" }; } if (todaySchedule && todaySchedule.works && currentTime < todaySchedule.start) { return { text: `Indisponível no momento, retorna hoje às ${todaySchedule.start}`, color: "#fca5a5", dotColor: "#ef4444" }; } const daysPt = { monday: "Segunda", tuesday: "Terça", wednesday: "Quarta", thursday: "Quinta", friday: "Sexta", saturday: "Sábado", sunday: "Domingo" }; for (let i = 1; i <= 7; i++) { const nextIndex = (now.getDay() + i) % 7; const nextDayKey = daysOfWeek[nextIndex]; const nextSchedule = schedule[nextDayKey]; if (nextSchedule && nextSchedule.works) { const dayName = i === 1 ? "Amanhã" : daysPt[nextDayKey]; return { text: `Indisponível no momento. Retorna ${dayName} às ${nextSchedule.start}`, color: "#fca5a5", dotColor: "#ef4444" }; } } } return { text: "Indisponível no momento", color: "#fca5a5", dotColor: "#ef4444" }; } togglePlay(voiceId, audioUrl) { Object.values(this.audioElements).forEach(a => a.pause()); const audio = this.audioElements[voiceId]; if (!audio) { const newAudio = new Audio(audioUrl); this.audioElements[voiceId] = newAudio; newAudio.addEventListener('ended', () => { this.playingId = null; this.render(); }); newAudio.play(); this.playingId = voiceId; } else { if (this.playingId === voiceId) { audio.pause(); this.playingId = null; } else { audio.play(); this.playingId = voiceId; } } this.render(); } filterVoices() { if (this.selectedCategory === 'duty') { const now = new Date(); const today = now.toISOString().split('T')[0]; const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); const onDutyUserIds = new Set(); const onDutyVoiceIds = new Set(); this.voices.forEach(v => { let isOnDuty = false; // Plantão manual ativo if (v.on_duty && v.duty_date === today && v.duty_end_time >= currentTime) { isOnDuty = true; } // Horário automático de gravação if (!isOnDuty && v.recording_schedule) { const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayKey = days[now.getDay()]; const sched = v.recording_schedule[dayKey]; if (sched && sched.works && currentTime >= sched.start && currentTime <= sched.end) { isOnDuty = true; } } if (isOnDuty) { if (v.user_id) { onDutyUserIds.add(v.user_id); } else { onDutyVoiceIds.add(v.id); } } }); return this.voices.filter(v => { if (v.user_id && onDutyUserIds.has(v.user_id)) return true; if (onDutyVoiceIds.has(v.id)) return true; return false; }); } if (this.selectedCategory === 'all') return this.voices; if (this.selectedCategory === 'male') return this.voices.filter(v => v.gender === 'Masculino'); if (this.selectedCategory === 'female') return this.voices.filter(v => v.gender === 'Feminino'); if (this.selectedCategory === 'child') return this.voices.filter(v => v.is_child_voice === true); if (this.selectedCategory === 'impacto') return this.voices.filter(v => v.category_name === 'Impacto'); return this.voices.filter(v => v.category_id === this.selectedCategory); } hasActiveOnDuty() { const now = new Date(); const today = now.toISOString().split('T')[0]; const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); return this.voices.some(v => { // Plantão manual ativo if (v.on_duty && v.duty_date === today && v.duty_end_time >= currentTime) { return true; } // Horário automático de gravação if (v.recording_schedule) { const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayKey = days[now.getDay()]; const sched = v.recording_schedule[dayKey]; if (sched && sched.works && currentTime >= sched.start && currentTime <= sched.end) { return true; } } return false; }); } getOnDutyButtonText() { return "Gravando agora"; } handleOrderWithVoice(voiceId) { window.location.href = `${this.baseUrl}/OrderForm?voiceId=${voiceId}`; } render() { const filteredVoices = this.filterVoices(); const visibleVoices = filteredVoices.slice(0, this.visibleCount); this.shadowRoot.innerHTML = ''; this.addStyles(); const container = document.createElement('div'); container.className = 'container'; if (this.loadError) { container.innerHTML = `

Erro ao carregar locutores

Por favor, verifique o console para mais detalhes.

`; this.shadowRoot.appendChild(container); return; } if (this.voices.length === 0 && !this.loadError) { container.innerHTML = '
'; this.shadowRoot.appendChild(container); return; } const onDutyCount = this.voices.filter(v => v.is_available).length; const hasOnDuty = this.hasActiveOnDuty(); const dropdownCategories = this.categories .filter(cat => !['Impacto'].includes(cat.name)) .sort((a, b) => a.name === 'Animada' ? -1 : b.name === 'Animada' ? 1 : 0); const currentCategoryName = this.categories.find(c => c.id === this.selectedCategory)?.name || 'Mais filtros...'; const filtersDiv = document.createElement('div'); filtersDiv.className = 'filters'; filtersDiv.innerHTML = `
${hasOnDuty ? ` ` : ''}
`; container.appendChild(filtersDiv); filtersDiv.querySelectorAll('.filter-btn:not(.dropdown-trigger)').forEach(btn => { btn.addEventListener('click', (e) => { this.selectedCategory = e.target.dataset.filter; this.render(); }); }); const dropdownTrigger = filtersDiv.querySelector('.dropdown-trigger'); const dropdownMenu = filtersDiv.querySelector('.dropdown-menu'); if (dropdownTrigger && dropdownMenu) { dropdownTrigger.addEventListener('click', (e) => { e.stopPropagation(); dropdownMenu.style.display = dropdownMenu.style.display === 'none' ? 'block' : 'none'; }); document.addEventListener('click', () => { dropdownMenu.style.display = 'none'; }); dropdownMenu.querySelectorAll('.dropdown-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); this.selectedCategory = e.target.dataset.filter; dropdownMenu.style.display = 'none'; this.render(); }); item.addEventListener('mouseenter', (e) => { e.target.style.background = '#fef3c7'; }); item.addEventListener('mouseleave', (e) => { e.target.style.background = 'none'; }); }); } const grid = document.createElement('div'); grid.className = 'grid'; visibleVoices.forEach(voice => { const card = document.createElement('div'); card.className = 'voice-card'; card.style.backgroundImage = voice.profile_image_url ? `url('${voice.profile_image_url}')` : "url('https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68dfb8b4ca073475565efca7/6836915ba_bg-cards-locutores.jpg')"; const overlay = document.createElement('div'); overlay.className = 'voice-overlay'; const content = document.createElement('div'); content.className = 'voice-content'; const header = document.createElement('div'); header.className = 'voice-header'; const playBtn = document.createElement('button'); playBtn.className = 'play-btn'; playBtn.innerHTML = this.playingId === voice.id ? '' : ''; playBtn.addEventListener('click', (e) => { e.stopPropagation(); this.togglePlay(voice.id, voice.audio_sample_url); }); header.appendChild(playBtn); if (voice.category_name) { const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = voice.category_name; header.appendChild(badge); } const footer = document.createElement('div'); footer.className = 'voice-footer'; const name = document.createElement('h3'); name.className = 'voice-name'; name.textContent = voice.name; const availInfo = this.getAvailabilityInfo(voice); const status = document.createElement('div'); status.className = 'voice-status'; status.style.color = availInfo.color; status.innerHTML = `
${availInfo.text} `; const orderBtn = document.createElement('button'); orderBtn.className = 'order-btn'; orderBtn.innerHTML = ' Usar essa Voz'; orderBtn.addEventListener('click', () => { this.handleOrderWithVoice(voice.id); }); footer.appendChild(name); footer.appendChild(status); footer.appendChild(orderBtn); content.appendChild(header); content.appendChild(footer); card.appendChild(overlay); card.appendChild(content); grid.appendChild(card); }); container.appendChild(grid); if (filteredVoices.length > this.visibleCount) { const loadMoreDiv = document.createElement('div'); loadMoreDiv.className = 'load-more'; loadMoreDiv.innerHTML = ''; loadMoreDiv.querySelector('.load-more-btn').addEventListener('click', () => { this.visibleCount += 6; this.render(); }); container.appendChild(loadMoreDiv); } this.shadowRoot.appendChild(container); } } customElements.define('locutores-widget', LocutoresWidget); })();