Theme Customization
MYMUSICK includes two built-in themes that can be easily switched and customized.Switching Themes
Change the stylesheet reference inindex.html:11:
<link rel="stylesheet" href="estilooriginal.css">
Creating Custom Themes
Create new stylesheet
Copy one of the existing themes as a starting point:
cp estilooriginal.css my-custom-theme.css
Customize CSS variables
Both themes use CSS custom properties for easy customization:
:root {
--primary: #04CDA8; /* Main brand color */
--accent: #FF5757; /* Accent color */
--bg-dark: #111; /* Background */
--text-light: white; /* Text color */
}
Update typography
Change fonts to match your brand:
@import url('https://fonts.googleapis.com/css2?family=Your+Font+Here');
:root {
--font-display: 'Your Display Font', sans-serif;
--font-body: 'Your Body Font', sans-serif;
}
Theme Examples
- Neon Cyberpunk
- Minimalist Light
- Retro Vinyl
:root {
--primary: #ff00ff; /* Magenta */
--accent: #00ffff; /* Cyan */
--bg-dark: #0a0a0a; /* Deep black */
--text-light: #ffffff;
}
body {
background: linear-gradient(180deg, #0a0a0a 0%, #1a0a1a 100%);
text-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
}
.song:hover {
box-shadow: 0 0 20px var(--primary),
0 0 40px var(--accent);
}
:root {
--primary: #000000; /* Black */
--accent: #666666; /* Gray */
--bg-dark: #ffffff; /* White */
--text-light: #000000;
}
body {
background: #ffffff;
color: #000000;
}
.song {
background: #f5f5f5;
border: 1px solid #e0e0e0;
}
.song:hover {
background: #eeeeee;
border-color: #000000;
}
:root {
--primary: #f4a261; /* Warm orange */
--accent: #e76f51; /* Rust red */
--bg-dark: #264653; /* Deep teal */
--text-light: #e9c46a; /* Cream */
}
body {
background: linear-gradient(135deg, #264653 0%, #2a9d8f 100%);
font-family: 'Courier New', monospace;
}
.song {
background: rgba(244, 162, 97, 0.1);
border: 2px solid var(--accent);
}
Component Customization
Modify Search Behavior
Change Search Trigger
By default, search triggers on Enter key. To add instant search:// Replace the keydown listener (index.html:113)
input.addEventListener("input", debounce(async (e) => {
const query = input.value.trim();
if (query.length < 3) return; // Minimum 3 characters
results.innerHTML = `<p>Buscando... 🎵</p>`;
try {
const res = await fetch(
`https://mymusick-backend.onrender.com/search?q=${encodeURIComponent(query)}`
);
const songs = await res.json();
renderSongs(songs);
} catch {
results.innerHTML = `<p>Error al buscar 😕</p>`;
}
}, 500)); // 500ms debounce
// Add debounce helper function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
Add Search Filters
// Add filter buttons to HTML
<div class="filters">
<button data-filter="all" class="active">Todos</button>
<button data-filter="song">Canciones</button>
<button data-filter="artist">Artistas</button>
<button data-filter="album">Álbumes</button>
</div>
// Add filter functionality
let currentFilter = 'all';
document.querySelectorAll('[data-filter]').forEach(btn => {
btn.addEventListener('click', (e) => {
currentFilter = e.target.dataset.filter;
document.querySelectorAll('[data-filter]').forEach(b =>
b.classList.remove('active')
);
e.target.classList.add('active');
// Re-run search with filter
if (input.value.trim()) {
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
}
});
});
// Modify fetch to include filter
const res = await fetch(
`https://mymusick-backend.onrender.com/search?q=${encodeURIComponent(query)}&type=${currentFilter}`
);
Customize Song Cards
Add Duration Display
// Modify renderSongs function (index.html:152)
div.innerHTML = `
<img src="${song.thumbnail}"
alt="Portada de ${song.title}"
loading="lazy"
crossorigin="anonymous">
<div class="song-info">
<strong>${song.title}</strong>
<small>${song.artist}</small>
<span class="duration">${formatDuration(song.duration)}</span>
</div>
`;
// Add duration formatter
function formatDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
/* Add styling */
.duration {
font-size: 11px;
color: var(--muted);
margin-top: 4px;
display: block;
}
Add Favorite Button
// Add to song card HTML
div.innerHTML = `
<img src="${song.thumbnail}" ...>
<div class="song-info">
<strong>${song.title}</strong>
<small>${song.artist}</small>
</div>
<button class="favorite-btn" data-id="${song.id}"
onclick="toggleFavorite(event, '${song.id}')">
${isFavorite(song.id) ? '❤️' : '🤍'}
</button>
`;
// Favorite management
function toggleFavorite(event, songId) {
event.stopPropagation(); // Prevent song from playing
const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
const index = favorites.indexOf(songId);
if (index > -1) {
favorites.splice(index, 1);
} else {
favorites.push(songId);
}
localStorage.setItem('favorites', JSON.stringify(favorites));
event.target.textContent = isFavorite(songId) ? '❤️' : '🤍';
}
function isFavorite(songId) {
const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
return favorites.includes(songId);
}
Enhanced Player Controls
Add Volume Control
<!-- Add to footer.player -->
<div class="volume-control">
<span>🔊</span>
<input type="range" id="volumeSlider" min="0" max="100" value="100">
</div>
const volumeSlider = document.getElementById('volumeSlider');
volumeSlider.addEventListener('input', (e) => {
if (player && player.setVolume) {
player.setVolume(e.target.value);
}
});
.volume-control {
display: flex;
align-items: center;
gap: 10px;
}
#volumeSlider {
width: 100px;
height: 4px;
background: var(--surface2);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
#volumeSlider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
}
Add Progress Bar
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
<div class="progress-time">
<span id="currentTime">0:00</span>
<span id="totalTime">0:00</span>
</div>
</div>
let progressInterval;
function updateProgress() {
if (!player || !player.getDuration) return;
const current = player.getCurrentTime();
const total = player.getDuration();
const percentage = (current / total) * 100;
document.getElementById('progressFill').style.width = `${percentage}%`;
document.getElementById('currentTime').textContent = formatTime(current);
document.getElementById('totalTime').textContent = formatTime(total);
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Start updating progress when playing
function onPlayerStateChange(event) {
isPlaying = event.data === YT.PlayerState.PLAYING;
if (isPlaying) {
progressInterval = setInterval(updateProgress, 1000);
} else {
clearInterval(progressInterval);
}
playPauseBtn.classList.remove("hidden");
playPauseBtn.textContent = isPlaying ? "⏸️ Pausar" : "▶️ Reproducir";
}
Backend Customization
Using a Custom Backend
Replace the backend URL to use your own API:// Add configuration object
const CONFIG = {
API_BASE_URL: 'https://your-backend.com',
ENDPOINTS: {
search: '/api/v1/search',
trending: '/api/v1/trending',
playlist: '/api/v1/playlist'
}
};
// Update search function
const res = await fetch(
`${CONFIG.API_BASE_URL}${CONFIG.ENDPOINTS.search}?q=${encodeURIComponent(query)}`
);
Expected Backend Response Format
[
{
"id": "dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"artist": "Rick Astley",
"thumbnail": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg",
"duration": 213
}
]
Make sure your backend supports CORS and returns proper headers:
Access-Control-Allow-Origin: *
Content-Type: application/json
Add Caching Layer
const cache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async function searchWithCache(query) {
const cacheKey = `search_${query}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const res = await fetch(
`https://mymusick-backend.onrender.com/search?q=${encodeURIComponent(query)}`
);
const data = await res.json();
cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
}
// Use in search handler
input.addEventListener("keydown", async (e) => {
if (e.key !== "Enter") return;
const query = input.value.trim();
if (!query) return;
results.innerHTML = `<p>Buscando... 🎵</p>`;
try {
const songs = await searchWithCache(query);
renderSongs(songs);
} catch {
results.innerHTML = `<p>Error al buscar 😕</p>`;
}
});
Advanced Features
Add Playlist Support
let playlist = [];
let currentSongIndex = 0;
function addToPlaylist(song) {
playlist.push(song);
updatePlaylistUI();
}
function playNext() {
if (currentSongIndex < playlist.length - 1) {
currentSongIndex++;
loadSong(playlist[currentSongIndex]);
}
}
function playPrevious() {
if (currentSongIndex > 0) {
currentSongIndex--;
loadSong(playlist[currentSongIndex]);
}
}
// Auto-play next song when current ends
function onPlayerStateChange(event) {
if (event.data === YT.PlayerState.ENDED) {
playNext();
}
// ... rest of existing code
}
Add Keyboard Shortcuts
document.addEventListener('keydown', (e) => {
// Space: Play/Pause
if (e.code === 'Space' && e.target.tagName !== 'INPUT') {
e.preventDefault();
togglePlayPause();
}
// Arrow Right: Next song
if (e.code === 'ArrowRight' && playlist.length > 0) {
playNext();
}
// Arrow Left: Previous song
if (e.code === 'ArrowLeft' && playlist.length > 0) {
playPrevious();
}
// Arrow Up: Volume up
if (e.code === 'ArrowUp' && player) {
e.preventDefault();
const currentVol = player.getVolume();
player.setVolume(Math.min(100, currentVol + 10));
}
// Arrow Down: Volume down
if (e.code === 'ArrowDown' && player) {
e.preventDefault();
const currentVol = player.getVolume();
player.setVolume(Math.max(0, currentVol - 10));
}
});
Add Analytics Tracking
function trackEvent(category, action, label) {
// Google Analytics
if (window.gtag) {
gtag('event', action, {
'event_category': category,
'event_label': label
});
}
// Or custom analytics
fetch('https://your-analytics.com/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category, action, label })
});
}
// Track searches
input.addEventListener("keydown", async (e) => {
if (e.key !== "Enter") return;
const query = input.value.trim();
trackEvent('Search', 'query', query);
// ... rest of search code
});
// Track song plays
function loadSong(song) {
trackEvent('Player', 'play', song.title);
// ... rest of loadSong code
}
Testing Your Customizations
Next Steps
Setup
Review development environment setup
Architecture
Deep dive into application architecture
