Teste de filtros de projetos
Deploy / Deploy (push) Successful in 1m22s Details

This commit is contained in:
JotaChina 2025-12-13 22:05:42 -03:00
parent dd837d6d6b
commit 984fdf2ca5
1 changed files with 218 additions and 220 deletions

View File

@ -20,63 +20,72 @@ permalink: /projetos/
<div style="width: 60px; height: 4px; background-color: #3273dc; margin: 1rem auto; border-radius: 2px;"></div>
</div>
<div class="box has-background-white-ter mb-6" style="border: 1px solid #dbdbdb;">
<div class="columns is-vcentered">
<div class="column is-narrow">
<span class="icon-text has-text-weight-bold has-text-grey">
<span class="icon"><i class="fa-solid fa-robot"></i></span>
<span>Filtro IA:</span>
</span>
<div class="box has-background-white-ter mb-6 p-0" style="border: 1px solid #dbdbdb; overflow: hidden;">
<div class="p-3 is-clickable" id="filter-toggle-btn" style="cursor: pointer; display: flex; align-items: center; justify-content: space-between; background-color: #f5f5f5;">
<div class="icon-text has-text-weight-bold has-text-grey-dark">
<span class="icon"><i class="fa-solid fa-sliders"></i></span>
<span>Filtros, Gêneros & Ordenação</span>
</div>
<div class="column">
<div class="buttons" id="genre-filters">
<button class="button is-small is-rounded is-active is-info" data-genre="all">Todos</button>
<span class="icon transition-icon">
<i class="fa-solid fa-chevron-down" id="filter-chevron"></i>
</span>
</div>
<div id="filter-content" style="display: none; border-top: 1px solid #e8e8e8;">
<div class="p-4">
<div class="columns is-vcentered is-multiline">
<div class="column is-narrow">
<span class="tag is-info is-light has-text-weight-bold">
<i class="fa-solid fa-robot mr-1"></i> Filtro IA:
</span>
</div>
</div>
<div class="column is-narrow">
<div class="field has-addons">
<div class="control">
<span class="button is-static is-small is-rounded">
<i class="fa-solid fa-star"></i>
</span>
</div>
<div class="control">
<div class="select is-small">
<select id="star-filter">
<option value="all">Todas Complexidades</option>
<option value="5">⭐⭐⭐⭐⭐ (5) Expert</option>
<option value="4">⭐⭐⭐⭐ (4+) Avançado</option>
<option value="3">⭐⭐⭐ (3+) Intermediário</option>
<option value="2">⭐⭐ (2+) Básico</option>
</select>
</div>
</div>
<div class="control"><span class="mx-1"></span></div>
<div class="control">
<a class="button is-static is-small is-rounded">
<i class="fa-solid fa-sort"></i>
</a>
</div>
<div class="control">
<div class="select is-small is-rounded">
<select id="sort-select">
<option value="default">Ordem Padrão</option>
<option value="stars_desc">⭐ Mais Complexos</option>
<option value="stars_asc">⭐ Mais Simples</option>
<option value="intensity_desc">🔥 Mais Intensos (dB)</option>
<option value="intensity_asc">❄️ Mais Calmos (dB)</option>
<option value="bpm_desc">⚡ BPM (Rápido)</option>
<option value="bpm_asc">🐢 BPM (Lento)</option>
</select>
<div class="column">
<div class="buttons" id="genre-filters">
<button class="button is-small is-rounded is-active is-info" data-genre="all">Todos</button>
</div>
</div>
</div>
</div>
<div class="column is-narrow">
<div class="field has-addons" style="flex-wrap: wrap; gap: 5px;">
<div class="control has-icons-left">
<div class="select is-small is-rounded">
<select id="star-filter">
<option value="all">Todas Dificuldades</option>
<option value="5">⭐⭐⭐⭐⭐ Expert</option>
<option value="4">⭐⭐⭐⭐ Avançado</option>
<option value="3">⭐⭐⭐ Intermediário</option>
<option value="2">⭐⭐ Básico</option>
</select>
</div>
<div class="icon is-small is-left has-text-warning">
<i class="fa-solid fa-star"></i>
</div>
</div>
<div class="control has-icons-left">
<div class="select is-small is-rounded">
<select id="sort-select">
<option value="default">Ordem Padrão</option>
<option value="stars_desc">⭐ Mais Complexos</option>
<option value="stars_asc">⭐ Mais Simples</option>
<option value="intensity_desc">🔥 Mais Intensos</option>
<option value="intensity_asc">❄️ Mais Calmos</option>
<option value="bpm_desc">⚡ BPM (Rápido)</option>
</select>
</div>
<div class="icon is-small is-left">
<i class="fa-solid fa-sort"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -283,8 +292,36 @@ permalink: /projetos/
<script>
document.addEventListener('DOMContentLoaded', () => {
// ===============================================
// 1. CONFIGURAÇÃO E MODAL
// 1. CONFIGURAÇÃO VISUAL (MENU E MODAL)
// ===============================================
// --- Lógica do Menu Expansível ---
const filterBtn = document.getElementById('filter-toggle-btn');
const filterContent = document.getElementById('filter-content');
const filterChevron = document.getElementById('filter-chevron');
let isFilterOpen = false;
// Abre o menu automaticamente se a tela for grande (Desktop), fecha em Mobile
if(window.innerWidth > 1024) {
isFilterOpen = true;
filterContent.style.display = 'block';
filterChevron.style.transform = 'rotate(180deg)';
}
if(filterBtn && filterContent) {
filterBtn.addEventListener('click', () => {
isFilterOpen = !isFilterOpen;
if(isFilterOpen) {
filterContent.style.display = 'block';
filterChevron.style.transform = 'rotate(180deg)';
} else {
filterContent.style.display = 'none';
filterChevron.style.transform = 'rotate(0deg)';
}
});
}
// --- Lógica do Modal (Preview) ---
const modal = document.getElementById('preview-modal');
const iframe = document.getElementById('preview-iframe');
const modalTitle = document.getElementById('modal-title');
@ -293,10 +330,7 @@ document.addEventListener('DOMContentLoaded', () => {
function normalizarChaveJS(str) {
if (!str) return "";
return str.toString()
.toLowerCase()
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]/g, "");
return str.toString().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
}
function openModal(url, title, btnText, btnLink) {
@ -326,212 +360,176 @@ document.addEventListener('DOMContentLoaded', () => {
closeButtons.forEach(el => el.addEventListener('click', closeModal));
document.addEventListener('keydown', (e) => { if (e.key === "Escape") closeModal(); });
// ===============================================
// 2. LÓGICA DE DADOS (IA / FILTROS / ORDENAÇÃO)
// 2. LÓGICA DE DADOS (IA / FILTROS)
// ===============================================
const JSON_URL = '/mmpSearch/src_mmpSearch/saida_analises/db_final_completo.json';
const cards = document.querySelectorAll('.project-card');
const genreContainer = document.getElementById('genre-filters');
const sortSelect = document.getElementById('sort-select');
const starFilterSelect = document.getElementById('star-filter'); // NOVO SELETOR
const starFilterSelect = document.getElementById('star-filter');
const projectsContainer = document.getElementById('projects-container');
// Estado global dos filtros
let currentGenre = 'all';
let currentMinStars = 'all';
// Inicializa a carga de dados
fetch(JSON_URL)
.then(response => {
if (!response.ok) throw new Error("JSON não encontrado: " + response.statusText);
return response.json();
})
.then(r => r.ok ? r.json() : Promise.reject("Erro JSON"))
.then(data => {
console.log("Dados IA carregados:", data.length, "músicas.");
enrichCards(data);
createGenreButtons(data);
})
.catch(err => {
console.warn("Aviso: Dados da IA não puderam ser carregados.", err);
const filterBox = document.querySelector('.box.has-background-white-ter');
if(filterBox) filterBox.style.display = 'none';
});
.catch(console.warn);
// Função para desenhar as estrelas com COR FIXA (Gold)
function gerarEstrelas(num) {
// Garante que num seja inteiro
const n = Math.round(num);
let html = '<span class="has-text-warning" title="Nível de Complexidade: '+n+'/5">';
const n = Math.round(num) || 0;
// Cor #f1c40f é o amarelo padrão de estrelas
let html = `<div style="color: #f1c40f; letter-spacing: 2px;" title="Complexidade: ${n}/5">`;
for (let i = 1; i <= 5; i++) {
if (i <= n) {
html += '<i class="fa-solid fa-star"></i>';
} else {
html += '<i class="fa-regular fa-star" style="opacity:0.4"></i>';
// Estrela vazia em cinza claro para contraste
html += '<i class="fa-regular fa-star" style="color: #dbdbdb;"></i>';
}
}
html += '</span>';
html += '</div>';
return html;
}
function enrichCards(data) {
const mapDados = {};
data.forEach(item => {
const keyArquivo = normalizarChaveJS(item.arquivo.replace('.wav','').replace('.mp3',''));
mapDados[keyArquivo] = item;
if(item.dados_projeto && item.dados_projeto.titulo) {
const keyTitulo = normalizarChaveJS(item.dados_projeto.titulo);
mapDados[keyTitulo] = item;
}
const k1 = normalizarChaveJS(item.arquivo.replace('.wav','').replace('.mp3',''));
mapDados[k1] = item;
if(item.dados_projeto?.titulo) mapDados[normalizarChaveJS(item.dados_projeto.titulo)] = item;
});
cards.forEach(card => {
const cardKey = normalizarChaveJS(card.dataset.title);
const info = mapDados[cardKey];
const info = mapDados[normalizarChaveJS(card.dataset.title)];
// Defaults
let genero = "Unknown";
let estrelas = 0;
let intensidade = -100;
let bpm = 0;
let tomHtml = "";
if (info) {
// --- 1. Extração de Dados ---
const genero = (info.analise_ia?.genero_macro) || "Unknown";
const intensidade = (info.analise_tecnica?.intensidade_db) ? parseFloat(info.analise_tecnica.intensidade_db) : -100;
const bpm = (info.analise_tecnica?.bpm) ? parseFloat(info.analise_tecnica.bpm) : 0;
// Extração segura das estrelas
let estrelas = 0;
if (info.analise_tecnica?.complexidade?.estrelas) {
estrelas = parseInt(info.analise_tecnica.complexidade.estrelas);
}
genero = info.analise_ia?.genero_macro || "Unknown";
intensidade = parseFloat(info.analise_tecnica?.intensidade_db || -100);
bpm = parseFloat(info.analise_tecnica?.bpm || 0);
estrelas = parseInt(info.analise_tecnica?.complexidade?.estrelas || 0);
// --- 2. Atualiza Dataset do HTML (Para filtros funcionarem) ---
card.dataset.genre = genero;
card.dataset.intensity = intensidade;
card.dataset.bpm_real = bpm;
card.dataset.stars = estrelas; // Importante para ordenação
// HTML do Tom
if (info.analise_tecnica?.tom) {
const t = info.analise_tecnica.tom;
const e = info.analise_tecnica.escala === 'minor' ? 'm' : '';
tomHtml = `<span class="tag is-white is-rounded border-tag">🎹 ${t}${e}</span>`;
}
}
// --- 3. Renderização Visual ---
const bpmContainer = card.querySelector('.bpm-container');
if (bpmContainer) {
const iaTagsDiv = document.createElement('div');
iaTagsDiv.className = "tags is-centered mt-1 mb-3";
iaTagsDiv.style.flexDirection = "column";
// Salva no dataset para filtros
card.dataset.genre = genero;
card.dataset.stars = estrelas;
card.dataset.intensity = intensidade;
card.dataset.bpm_real = bpm;
let htmlFinal = '';
// Injeta HTML
const bpmContainer = card.querySelector('.bpm-container');
if (bpmContainer) {
// Limpa conteúdo anterior injetado via JS se houver (opcional)
// bpmContainer.innerHTML = ''; // Cuidado se tiver o BPM original do Liquid aqui.
const iaDiv = document.createElement('div');
iaDiv.style.display = "flex";
iaDiv.style.flexDirection = "column";
iaDiv.style.alignItems = "center";
iaDiv.style.gap = "4px";
iaDiv.className = "mt-2";
// Renderiza Estrelas (mesmo que seja 0, mostra estrelas vazias)
htmlFinal += `<div class="mb-1" style="font-size: 0.8rem;">${gerarEstrelas(estrelas)}</div>`;
htmlFinal += '<div class="tags is-centered">';
// 1. Estrelas
const starsDiv = document.createElement('div');
starsDiv.innerHTML = gerarEstrelas(estrelas);
iaDiv.appendChild(starsDiv);
// Tag Gênero
if (genero !== "Unknown") {
let colorClass = 'is-primary';
if(genero === 'Electronic') colorClass = 'is-info';
if(genero === 'Hip Hop') colorClass = 'is-warning';
if(genero === 'Rock') colorClass = 'is-danger';
// 2. Tags (Gênero + Tom)
const tagsRow = document.createElement('div');
tagsRow.className = "tags is-centered mb-0";
if(genero !== "Unknown") {
let color = 'is-primary';
if(genero === 'Electronic') color = 'is-info';
if(genero === 'Hip Hop') color = 'is-warning';
if(genero === 'Rock') color = 'is-danger';
tagsRow.innerHTML += `<span class="tag ${color} is-light is-rounded mr-1">🤖 ${genero}</span>`;
}
tagsRow.innerHTML += tomHtml;
iaDiv.appendChild(tagsRow);
htmlFinal += `<span class="tag ${colorClass} is-light is-rounded mr-1" style="font-size: 0.65rem;">
🤖 ${genero}
</span>`;
}
// Tag Tom
if (info.analise_tecnica?.tom) {
const nota = info.analise_tecnica.tom;
const escala = info.analise_tecnica.escala === 'minor' ? 'm' : '';
htmlFinal += `<span class="tag is-white is-rounded" style="font-size: 0.65rem; border: 1px solid #ddd; color: #555;">
🎹 ${nota}${escala}
</span>`;
}
htmlFinal += '</div>';
iaTagsDiv.innerHTML = htmlFinal;
bpmContainer.appendChild(iaTagsDiv);
}
} else {
// Valores padrão se não achar dados
card.dataset.genre = "Outros";
card.dataset.intensity = -100;
card.dataset.stars = 0;
bpmContainer.appendChild(iaDiv);
}
});
}
function createGenreButtons(data) {
const genres = new Set();
data.forEach(item => {
if(item.analise_ia?.genero_macro && item.analise_ia.genero_macro !== "Unknown") {
genres.add(item.analise_ia.genero_macro);
}
data.forEach(i => {
if(i.analise_ia?.genero_macro && i.analise_ia.genero_macro !== "Unknown")
genres.add(i.analise_ia.genero_macro);
});
const genresArray = Array.from(genres).sort();
genresArray.forEach(genre => {
Array.from(genres).sort().forEach(g => {
const btn = document.createElement('button');
btn.className = 'button is-small is-rounded is-white'; // Começa branco
btn.textContent = genre;
btn.dataset.genre = genre;
btn.addEventListener('click', () => {
// UI Update
const allBtns = genreContainer.querySelectorAll('.button');
allBtns.forEach(b => {
btn.className = 'button is-small is-rounded is-white';
btn.textContent = g;
btn.onclick = () => {
document.querySelectorAll('#genre-filters .button').forEach(b => {
b.classList.remove('is-active', 'is-info');
b.classList.add('is-white');
});
btn.classList.remove('is-white');
btn.classList.add('is-active', 'is-info');
// Logic Update
currentGenre = genre;
currentGenre = g;
applyFilters();
});
};
genreContainer.appendChild(btn);
});
// Botão Todos
// Reset All Button
const allBtn = genreContainer.querySelector('[data-genre="all"]');
allBtn.addEventListener('click', () => {
genreContainer.querySelectorAll('.button').forEach(b => {
b.classList.remove('is-active', 'is-info');
b.classList.add('is-white');
allBtn.onclick = () => {
document.querySelectorAll('#genre-filters .button').forEach(b => {
b.classList.remove('is-active', 'is-info');
b.classList.add('is-white');
});
allBtn.classList.remove('is-white');
allBtn.classList.add('is-active', 'is-info');
currentGenre = 'all';
applyFilters();
});
};
}
// Lógica Unificada de Filtros
function applyFilters() {
cards.forEach(card => {
const column = card.closest('.project-column');
cards.forEach(c => {
const col = c.closest('.project-column');
const g = c.dataset.genre;
const s = parseInt(c.dataset.stars || 0);
// 1. Checa Gênero
const cardGenre = card.dataset.genre;
const matchGenre = (currentGenre === 'all' || cardGenre === currentGenre);
const matchG = (currentGenre === 'all' || g === currentGenre);
const matchS = (currentMinStars === 'all' || s >= parseInt(currentMinStars));
// 2. Checa Estrelas (Mínimo)
const cardStars = parseInt(card.dataset.stars) || 0;
let matchStars = true;
if (currentMinStars !== 'all') {
const min = parseInt(currentMinStars);
matchStars = (cardStars >= min);
}
// Exibe apenas se passar nos DOIS filtros
if (matchGenre && matchStars) {
column.style.display = 'block';
} else {
column.style.display = 'none';
}
});
col.style.display = (matchG && matchS) ? 'block' : 'none';
});
}
// Evento do Dropdown de Estrelas
if(starFilterSelect) {
starFilterSelect.addEventListener('change', (e) => {
currentMinStars = e.target.value;
@ -539,37 +537,37 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Ordenação
sortSelect.addEventListener('change', (e) => {
const criteria = e.target.value;
const columns = Array.from(projectsContainer.children);
columns.sort((colA, colB) => {
const cardA = colA.querySelector('.project-card');
const cardB = colB.querySelector('.project-card');
const intensityA = parseFloat(cardA.dataset.intensity) || -100;
const intensityB = parseFloat(cardB.dataset.intensity) || -100;
const bpmA = parseFloat(cardA.dataset.bpm_real) || 0;
const bpmB = parseFloat(cardB.dataset.bpm_real) || 0;
const starsA = parseInt(cardA.dataset.stars) || 0;
const starsB = parseInt(cardB.dataset.stars) || 0;
if (criteria === 'intensity_desc') return intensityB - intensityA;
if (criteria === 'intensity_asc') return intensityA - intensityB;
if (criteria === 'bpm_desc') return bpmB - bpmA;
if (criteria === 'bpm_asc') return bpmA - bpmB;
// Novas ordenações
if (criteria === 'stars_desc') return starsB - starsA;
if (criteria === 'stars_asc') return starsA - starsB;
const titleA = cardA.dataset.title.toLowerCase();
const titleB = cardB.dataset.title.toLowerCase();
return titleA.localeCompare(titleB);
const crit = e.target.value;
const cols = Array.from(projectsContainer.children);
cols.sort((a, b) => {
const cA = a.querySelector('.project-card');
const cB = b.querySelector('.project-card');
const getVal = (el, k) => parseFloat(el.dataset[k] || 0);
if(crit === 'stars_desc') return getVal(cB, 'stars') - getVal(cA, 'stars');
if(crit === 'stars_asc') return getVal(cA, 'stars') - getVal(cB, 'stars');
if(crit === 'bpm_desc') return getVal(cB, 'bpm_real') - getVal(cA, 'bpm_real');
if(crit === 'intensity_desc') return getVal(cB, 'intensity') - getVal(cA, 'intensity');
if(crit === 'intensity_asc') return getVal(cA, 'intensity') - getVal(cB, 'intensity');
return cA.dataset.title.localeCompare(cB.dataset.title);
});
columns.forEach(col => projectsContainer.appendChild(col));
cols.forEach(c => projectsContainer.appendChild(c));
});
});
</script>
</script>
<style>
.transition-icon i {
transition: transform 0.3s ease;
}
.border-tag {
border: 1px solid #dbdbdb;
color: #555;
}
</style>