style: auto-fix linting issues

This commit is contained in:
SamidyFR 2026-02-19 13:55:31 +00:00 committed by github-actions[bot]
parent 5eba6aa224
commit 48937ed573
5 changed files with 485 additions and 219 deletions

View file

@ -738,95 +738,272 @@
<div class="modal-overlay"></div>
<div class="modal-content">
<h3>Edit Profile</h3>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Username</label>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Username</label>
<input type="text" id="edit-profile-username" class="template-input" placeholder="username" />
<p id="username-error" style="color: #ef4444; font-size: 0.8rem; display: none; margin-top: 0.25rem;"></p>
<p
id="username-error"
style="color: #ef4444; font-size: 0.8rem; display: none; margin-top: 0.25rem"
></p>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Display Name</label>
<input type="text" id="edit-profile-display-name" class="template-input" placeholder="Display Name" />
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Display Name</label>
<input
type="text"
id="edit-profile-display-name"
class="template-input"
placeholder="Display Name"
/>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Avatar URL</label>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Avatar URL</label>
<div style="display: flex; gap: 0.5rem; align-items: stretch">
<input type="url" id="edit-profile-avatar" class="template-input" placeholder="Avatar URL" style="flex: 1; margin: 0; display: none" />
<input
type="url"
id="edit-profile-avatar"
class="template-input"
placeholder="Avatar URL"
style="flex: 1; margin: 0; display: none"
/>
<input type="file" id="edit-profile-avatar-file" accept="image/*" style="display: none" />
<button type="button" id="edit-profile-avatar-upload-btn" class="template-btn" style="flex: 1; padding: 0.5rem; font-size: 0.85rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button
type="button"
id="edit-profile-avatar-upload-btn"
class="template-btn"
style="
flex: 1;
padding: 0.5rem;
font-size: 0.85rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: var(--input);
border: 1px solid var(--border);
color: var(--foreground);
border-radius: var(--radius);
cursor: pointer;
"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span>Upload</span>
</button>
<button type="button" id="edit-profile-avatar-toggle-btn" class="template-btn" style="padding: 0.5rem; font-size: 0.85rem; white-space: nowrap; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;" title="Switch to URL input">or URL</button>
<button
type="button"
id="edit-profile-avatar-toggle-btn"
class="template-btn"
style="
padding: 0.5rem;
font-size: 0.85rem;
white-space: nowrap;
background: var(--input);
border: 1px solid var(--border);
color: var(--foreground);
border-radius: var(--radius);
cursor: pointer;
"
title="Switch to URL input"
>
or URL
</button>
</div>
<div id="edit-profile-avatar-upload-status" style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"></div>
<div
id="edit-profile-avatar-upload-status"
style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"
></div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Banner URL</label>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Banner URL</label>
<div style="display: flex; gap: 0.5rem; align-items: stretch">
<input type="url" id="edit-profile-banner" class="template-input" placeholder="Banner URL" style="flex: 1; margin: 0; display: none" />
<input
type="url"
id="edit-profile-banner"
class="template-input"
placeholder="Banner URL"
style="flex: 1; margin: 0; display: none"
/>
<input type="file" id="edit-profile-banner-file" accept="image/*" style="display: none" />
<button type="button" id="edit-profile-banner-upload-btn" class="template-btn" style="flex: 1; padding: 0.5rem; font-size: 0.85rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button
type="button"
id="edit-profile-banner-upload-btn"
class="template-btn"
style="
flex: 1;
padding: 0.5rem;
font-size: 0.85rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: var(--input);
border: 1px solid var(--border);
color: var(--foreground);
border-radius: var(--radius);
cursor: pointer;
"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span>Upload</span>
</button>
<button type="button" id="edit-profile-banner-toggle-btn" class="template-btn" style="padding: 0.5rem; font-size: 0.85rem; white-space: nowrap; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;" title="Switch to URL input">or URL</button>
<button
type="button"
id="edit-profile-banner-toggle-btn"
class="template-btn"
style="
padding: 0.5rem;
font-size: 0.85rem;
white-space: nowrap;
background: var(--input);
border: 1px solid var(--border);
color: var(--foreground);
border-radius: var(--radius);
cursor: pointer;
"
title="Switch to URL input"
>
or URL
</button>
</div>
<div id="edit-profile-banner-upload-status" style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"></div>
<div
id="edit-profile-banner-upload-status"
style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"
></div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Status (Listening to...)</label>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
>Status (Listening to...)</label
>
<div class="status-picker-container">
<input type="text" id="edit-profile-status-search" class="template-input" placeholder="Search for a song or album..." autocomplete="off" />
<input
type="text"
id="edit-profile-status-search"
class="template-input"
placeholder="Search for a song or album..."
autocomplete="off"
/>
<div id="status-search-results" class="search-results-dropdown"></div>
<input type="hidden" id="edit-profile-status-json">
<div id="status-preview" style="display: none; margin-top: 0.5rem; padding: 0.5rem; background: var(--secondary); border-radius: var(--radius); align-items: center; gap: 0.5rem;">
<img id="status-preview-img" src="" style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover;">
<div style="flex: 1; min-width: 0;">
<div id="status-preview-title" style="font-weight: 500; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
<div id="status-preview-subtitle" style="font-size: 0.8rem; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
<input type="hidden" id="edit-profile-status-json" />
<div
id="status-preview"
style="
display: none;
margin-top: 0.5rem;
padding: 0.5rem;
background: var(--secondary);
border-radius: var(--radius);
align-items: center;
gap: 0.5rem;
"
>
<img
id="status-preview-img"
src=""
style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover"
/>
<div style="flex: 1; min-width: 0">
<div
id="status-preview-title"
style="
font-weight: 500;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
></div>
<div
id="status-preview-subtitle"
style="
font-size: 0.8rem;
color: var(--muted-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
></div>
</div>
<button id="clear-status-btn" class="btn-icon" style="width: 24px; height: 24px;">&times;</button>
<button id="clear-status-btn" class="btn-icon" style="width: 24px; height: 24px">
&times;
</button>
</div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Favorite Albums (Max 5)</label>
<div id="edit-favorite-albums-list" style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.5rem;"></div>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
>Favorite Albums (Max 5)</label
>
<div
id="edit-favorite-albums-list"
style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.5rem"
></div>
<div class="status-picker-container">
<input type="text" id="edit-favorite-albums-search" class="template-input" placeholder="Search for an album..." autocomplete="off" />
<input
type="text"
id="edit-favorite-albums-search"
class="template-input"
placeholder="Search for an album..."
autocomplete="off"
/>
<div id="edit-favorite-albums-results" class="search-results-dropdown"></div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">About Me</label>
<textarea id="edit-profile-about" class="template-input" style="resize: vertical; min-height: 80px;" placeholder="Tell us about yourself"></textarea>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">About Me</label>
<textarea
id="edit-profile-about"
class="template-input"
style="resize: vertical; min-height: 80px"
placeholder="Tell us about yourself"
></textarea>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Website</label>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Website</label>
<input type="url" id="edit-profile-website" class="template-input" placeholder="https://..." />
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Last.fm Username</label>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Last.fm Username</label>
<input type="text" id="edit-profile-lastfm" class="template-input" placeholder="Last.fm Username" />
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.5rem; line-height: 1.5;">Integrating Last.fm enables recent activity and top stats on your profile. Authorize it in <strong>Settings &gt; Scrobbling</strong>. Note: Last.fm authorization is stored locally and must be repeated on each device.</p>
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.5rem; line-height: 1.5">
Integrating Last.fm enables recent activity and top stats on your profile. Authorize it in
<strong>Settings &gt; Scrobbling</strong>. Note: Last.fm authorization is stored locally and
must be repeated on each device.
</p>
</div>
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem; font-size: 1rem;">Privacy</h4>
<div class="setting-item" style="padding: 0.5rem 0; border: none;">
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem; font-size: 1rem">Privacy</h4>
<div class="setting-item" style="padding: 0.5rem 0; border: none">
<div class="info"><span class="label">Public Playlists</span></div>
<label class="toggle-switch"><input type="checkbox" id="privacy-playlists-toggle" checked><span class="slider"></span></label>
<label class="toggle-switch"
><input type="checkbox" id="privacy-playlists-toggle" checked /><span class="slider"></span
></label>
</div>
<div class="setting-item" style="padding: 0.5rem 0; border: none;">
<div class="setting-item" style="padding: 0.5rem 0; border: none">
<div class="info"><span class="label">Show Last.fm Link & Stats</span></div>
<label class="toggle-switch"><input type="checkbox" id="privacy-lastfm-toggle" checked><span class="slider"></span></label>
<label class="toggle-switch"
><input type="checkbox" id="privacy-lastfm-toggle" checked /><span class="slider"></span
></label>
</div>
<div class="modal-actions">
@ -865,8 +1042,20 @@
<div class="modal-overlay"></div>
<div class="modal-content">
<h3 style="text-align: center; margin-bottom: 10px">Email Authentication</h3>
<input type="email" id="auth-email" class="template-input" placeholder="Email Address" style="margin-bottom: 0.5rem;" />
<input type="password" id="auth-password" class="template-input" placeholder="Password" style="margin-bottom: 1rem;" />
<input
type="email"
id="auth-email"
class="template-input"
placeholder="Email Address"
style="margin-bottom: 0.5rem"
/>
<input
type="password"
id="auth-password"
class="template-input"
placeholder="Password"
style="margin-bottom: 1rem"
/>
<div style="display: flex; gap: 10px; justify-content: center; margin-top: 10px">
<button id="email-signin-btn" class="btn-primary" style="flex: 1">Sign In</button>
<button id="email-signup-btn" class="btn-secondary" style="flex: 1">Sign Up</button>
@ -878,7 +1067,9 @@
>
Forgot Password?
</button>
<button id="cancel-email-auth-btn" class="btn-secondary" style="width: 100%; margin-top: 10px">Cancel</button>
<button id="cancel-email-auth-btn" class="btn-secondary" style="width: 100%; margin-top: 10px">
Cancel
</button>
</div>
</div>
@ -1475,8 +1666,20 @@
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="header-account-control" style="position: relative; margin-right: 1rem;">
<button id="header-account-btn" class="btn-icon" title="Account" style="width: 36px; height: 36px; border-radius: 50%; overflow: hidden; padding: 0; border: 1px solid var(--border);">
<div class="header-account-control" style="position: relative; margin-right: 1rem">
<button
id="header-account-btn"
class="btn-icon"
title="Account"
style="
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
padding: 0;
border: 1px solid var(--border);
"
>
<svg
id="header-account-icon"
xmlns="http://www.w3.org/2000/svg"
@ -1492,7 +1695,11 @@
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<img id="header-account-img" src="" style="width: 100%; height: 100%; object-fit: cover; display: none;">
<img
id="header-account-img"
src=""
style="width: 100%; height: 100%; object-fit: cover; display: none"
/>
</button>
<div id="header-account-dropdown" class="dropdown-menu"></div>
</div>
@ -2647,45 +2854,63 @@
<div class="profile-header-container">
<div class="profile-banner" id="profile-banner"></div>
<div class="profile-info-section">
<img id="profile-avatar" src="/assets/appicon.png" class="profile-avatar" alt="Avatar">
<img id="profile-avatar" src="/assets/appicon.png" class="profile-avatar" alt="Avatar" />
<div class="profile-details">
<h1 id="profile-display-name">User</h1>
<div id="profile-username" class="profile-username">@username</div>
<div id="profile-status" class="profile-status" style="display: none;"></div>
<div id="profile-status" class="profile-status" style="display: none"></div>
<div id="profile-about" class="profile-about"></div>
<div class="profile-links">
<a id="profile-website" href="#" target="_blank" class="profile-link" style="display: none;">Website</a>
<a id="profile-lastfm" href="#" target="_blank" class="profile-link" style="display: none;">Last.fm</a>
<a
id="profile-website"
href="#"
target="_blank"
class="profile-link"
style="display: none"
>Website</a
>
<a
id="profile-lastfm"
href="#"
target="_blank"
class="profile-link"
style="display: none"
>Last.fm</a
>
</div>
</div>
<div class="profile-actions">
<button id="profile-edit-btn" class="btn-secondary" style="display: none;">Edit Profile</button>
<button id="profile-edit-btn" class="btn-secondary" style="display: none">
Edit Profile
</button>
</div>
</div>
</div>
<div class="profile-content">
<h2 class="section-title">Public Playlists</h2>
<div class="card-grid" id="profile-playlists-container"></div>
<div id="profile-favorite-albums-section" style="display: none; margin-top: 3rem;">
<div id="profile-favorite-albums-section" style="display: none; margin-top: 3rem">
<h2 class="section-title">Favorite Albums of All Time</h2>
<div id="profile-favorite-albums-container"></div>
</div>
<div id="profile-recent-scrobbles-section" style="display: none; margin-top: 3rem;">
<div style="display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1rem;">
<h2 class="section-title" style="margin-bottom: 0;">Recent Scrobbling</h2>
<span style="font-size: 0.8rem; color: var(--muted-foreground);">Powered by Last.fm</span>
<div id="profile-recent-scrobbles-section" style="display: none; margin-top: 3rem">
<div style="display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1rem">
<h2 class="section-title" style="margin-bottom: 0">Recent Scrobbling</h2>
<span style="font-size: 0.8rem; color: var(--muted-foreground)"
>Powered by Last.fm</span
>
</div>
<div class="track-list" id="profile-recent-scrobbles-container"></div>
</div>
<div id="profile-top-artists-section" style="display: none; margin-top: 3rem;">
<div id="profile-top-artists-section" style="display: none; margin-top: 3rem">
<h2 class="section-title">Top Artists</h2>
<div class="card-grid" id="profile-top-artists-container"></div>
</div>
<div id="profile-top-albums-section" style="display: none; margin-top: 3rem;">
<div id="profile-top-albums-section" style="display: none; margin-top: 3rem">
<h2 class="section-title">Top Albums</h2>
<div class="card-grid" id="profile-top-albums-container"></div>
</div>
<div id="profile-top-tracks-section" style="display: none; margin-top: 3rem;">
<div id="profile-top-tracks-section" style="display: none; margin-top: 3rem">
<h2 class="section-title">Top Tracks</h2>
<div class="track-list" id="profile-top-tracks-container"></div>
</div>
@ -4350,9 +4575,13 @@
<div class="setting-item">
<div class="info">
<span class="label">Clear Cloud Data</span>
<span class="description">Delete all your data from the cloud (cannot be undone)</span>
<span class="description"
>Delete all your data from the cloud (cannot be undone)</span
>
</div>
<button id="firebase-clear-cloud-btn" class="btn-secondary danger">Clear Cloud Data</button>
<button id="firebase-clear-cloud-btn" class="btn-secondary danger">
Clear Cloud Data
</button>
</div>
<div class="setting-item">
<div class="info">
@ -4620,7 +4849,9 @@
>
<button id="firebase-connect-btn" class="btn-secondary">Connect with Google</button>
<button id="toggle-email-auth-btn" class="btn-secondary">Connect with Email</button>
<button id="view-my-profile-btn" class="btn-secondary" style="display: none;">View My Profile</button>
<button id="view-my-profile-btn" class="btn-secondary" style="display: none">
View My Profile
</button>
</div>
<p id="firebase-status" style="text-align: center; padding-top: 15px; color: #8b8b93">

View file

@ -5,7 +5,6 @@ import { MusicAPI } from './music-api.js';
import { apiSettings } from './storage.js';
import { debounce, escapeHtml } from './utils.js';
// objects execution february 29th 2027
const profilePage = document.getElementById('page-profile');
@ -101,7 +100,9 @@ function setupImageUploadControl(idPrefix) {
urlInput.value = url;
statusEl.textContent = 'Done!';
statusEl.style.color = '#10b981';
setTimeout(() => { statusEl.style.display = 'none'; }, 2000);
setTimeout(() => {
statusEl.style.display = 'none';
}, 2000);
} catch (error) {
statusEl.textContent = 'Failed - try URL';
statusEl.style.color = '#ef4444';
@ -123,9 +124,9 @@ const resetAvatarControl = setupImageUploadControl('edit-profile-avatar');
const resetBannerControl = setupImageUploadControl('edit-profile-banner');
export async function loadProfile(username) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.page').forEach((p) => p.classList.remove('active'));
profilePage.classList.add('active');
document.getElementById('profile-banner').style.backgroundImage = '';
document.getElementById('profile-avatar').src = '/assets/appicon.png';
document.getElementById('profile-display-name').textContent = 'Loading...';
@ -135,7 +136,7 @@ export async function loadProfile(username) {
document.getElementById('profile-website').style.display = 'none';
document.getElementById('profile-lastfm').style.display = 'none';
document.getElementById('profile-playlists-container').innerHTML = '';
const favAlbumsSection = document.getElementById('profile-favorite-albums-section');
const favAlbumsContainer = document.getElementById('profile-favorite-albums-container');
if (favAlbumsSection) favAlbumsSection.style.display = 'none';
@ -145,7 +146,7 @@ export async function loadProfile(username) {
const recentContainer = document.getElementById('profile-recent-scrobbles-container');
if (recentSection) recentSection.style.display = 'none';
if (recentContainer) recentContainer.innerHTML = '';
const topArtistsSection = document.getElementById('profile-top-artists-section');
const topArtistsContainer = document.getElementById('profile-top-artists-container');
const topAlbumsSection = document.getElementById('profile-top-albums-section');
@ -174,7 +175,7 @@ export async function loadProfile(username) {
document.getElementById('profile-display-name').textContent = profile.display_name || username;
if (profile.banner) document.getElementById('profile-banner').style.backgroundImage = `url('${profile.banner}')`;
if (profile.avatar_url) document.getElementById('profile-avatar').src = profile.avatar_url;
if (profile.status) {
const statusEl = document.getElementById('profile-status');
try {
@ -184,7 +185,10 @@ export async function loadProfile(username) {
<img src="${statusObj.image}" style="width: 20px; height: 20px; border-radius: 2px; vertical-align: middle; margin-right: 0.5rem;">
<a href="${statusObj.link}" class="status-link" style="color: inherit; text-decoration: none; font-weight: 500;">${statusObj.text}</a>
`;
statusEl.querySelector('.status-link').onclick = (e) => { e.preventDefault(); navigate(statusObj.link); };
statusEl.querySelector('.status-link').onclick = (e) => {
e.preventDefault();
navigate(statusObj.link);
};
} catch {
statusEl.textContent = `Listening to: ${profile.status}`;
}
@ -204,9 +208,10 @@ export async function loadProfile(username) {
if (profile.favorite_albums && profile.favorite_albums.length > 0) {
if (favAlbumsSection && favAlbumsContainer) {
favAlbumsSection.style.display = 'block';
favAlbumsContainer.innerHTML = profile.favorite_albums.map(album => {
const image = api.getCoverUrl(album.cover);
return `
favAlbumsContainer.innerHTML = profile.favorite_albums
.map((album) => {
const image = api.getCoverUrl(album.cover);
return `
<div class="favorite-album-item" style="display: flex; gap: 1rem; margin-bottom: 1rem; background: var(--card); padding: 1rem; border-radius: var(--radius); border: 1px solid var(--border);">
<div class="card" style="width: 120px; flex-shrink: 0; padding: 0; border: none; background: transparent; cursor: pointer;" onclick="window.location.hash='/album/${album.id}'">
<div class="card-image-wrapper" style="margin-bottom: 0.5rem;">
@ -223,13 +228,13 @@ export async function loadProfile(username) {
</div>
</div>
`;
}).join('');
})
.join('');
}
}
const dataSource = profile.profile_data_source || (profile.lastfm_username ? 'lastfm' : null);
if (profile.lastfm_username && profile.privacy?.lastfm !== 'private') {
const lfmEl = document.getElementById('profile-lastfm');
lfmEl.href = `https://last.fm/user/${profile.lastfm_username}`;
@ -237,26 +242,30 @@ export async function loadProfile(username) {
}
if (profile.lastfm_username && profile.privacy?.lastfm !== 'private') {
fetchLastFmRecentTracks(profile.lastfm_username).then(async tracks => {
fetchLastFmRecentTracks(profile.lastfm_username).then(async (tracks) => {
if (tracks.length > 0) {
recentSection.style.display = 'block';
recentContainer.innerHTML = tracks.map((track, index) => {
const isNowPlaying = track['@attr']?.nowplaying === 'true';
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
track._imgId = `scrobble-img-${index}`;
track._needsCover = !hasImage;
let dateDisplay = '';
if (isNowPlaying) dateDisplay = 'Scrobbling now';
else if (track.date) {
const date = new Date(track.date.uts * 1000);
dateDisplay = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
recentContainer.innerHTML = tracks
.map((track, index) => {
const isNowPlaying = track['@attr']?.nowplaying === 'true';
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
return `
track._imgId = `scrobble-img-${index}`;
track._needsCover = !hasImage;
let dateDisplay = '';
if (isNowPlaying) dateDisplay = 'Scrobbling now';
else if (track.date) {
const date = new Date(track.date.uts * 1000);
dateDisplay =
date.toLocaleDateString() +
' ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return `
<div class="track-item lastfm-track" data-title="${escapeHtml(track.name)}" data-artist="${escapeHtml(track.artist?.['#text'] || track.artist?.name || '')}" style="grid-template-columns: 40px 1fr auto; cursor: pointer;">
<img id="${track._imgId}" src="${image}" class="track-item-cover" style="width: 40px; height: 40px; border-radius: 4px;" loading="lazy" onerror="this.src='/assets/appicon.png'">
<div class="track-item-info">
@ -268,11 +277,15 @@ export async function loadProfile(username) {
<div class="track-item-duration" style="font-size: 0.8rem; min-width: auto;">${dateDisplay}</div>
</div>
`;
}).join('');
})
.join('');
recentContainer.querySelectorAll('.track-item').forEach(item => {
recentContainer.querySelectorAll('.track-item').forEach((item) => {
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
item.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
for (const track of tracks) {
@ -283,19 +296,20 @@ export async function loadProfile(username) {
}
});
fetchLastFmTopArtists(profile.lastfm_username).then(async artists => {
fetchLastFmTopArtists(profile.lastfm_username).then(async (artists) => {
if (artists.length > 0 && topArtistsSection && topArtistsContainer) {
topArtistsSection.style.display = 'block';
topArtistsContainer.innerHTML = artists.map((artist, index) => {
let image = getLastFmImage(artist.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
const imgId = `top-artist-img-${index}`;
artist._imgId = imgId;
artist._needsCover = !hasImage;
topArtistsContainer.innerHTML = artists
.map((artist, index) => {
let image = getLastFmImage(artist.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
return `
const imgId = `top-artist-img-${index}`;
artist._imgId = imgId;
artist._needsCover = !hasImage;
return `
<div class="card artist lastfm-card" data-name="${escapeHtml(artist.name)}" style="cursor: pointer;">
<div class="card-image-wrapper">
<img id="${imgId}" src="${image}" class="card-image" loading="lazy" onerror="this.src='/assets/appicon.png'">
@ -306,11 +320,15 @@ export async function loadProfile(username) {
</div>
</div>
`;
}).join('');
})
.join('');
topArtistsContainer.querySelectorAll('.card').forEach(card => {
topArtistsContainer.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => handleArtistClick(card.dataset.name));
card.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
for (const artist of artists) {
@ -321,22 +339,26 @@ export async function loadProfile(username) {
}
});
fetchLastFmTopAlbums(profile.lastfm_username).then(async albums => {
fetchLastFmTopAlbums(profile.lastfm_username).then(async (albums) => {
if (albums.length > 0 && topAlbumsSection && topAlbumsContainer) {
topAlbumsSection.style.display = 'block';
topAlbumsContainer.innerHTML = albums.map((album, index) => {
let image = getLastFmImage(album.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
topAlbumsContainer.innerHTML = albums
.map((album, index) => {
let image = getLastFmImage(album.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
const imgId = `top-album-img-${index}`;
album._imgId = imgId;
album._needsCover = !hasImage;
const artistName = album.artist?.name || album.artist?.['#text'] || (typeof album.artist === 'string' ? album.artist : 'Unknown Artist');
album._artistName = artistName;
const imgId = `top-album-img-${index}`;
album._imgId = imgId;
album._needsCover = !hasImage;
return `
const artistName =
album.artist?.name ||
album.artist?.['#text'] ||
(typeof album.artist === 'string' ? album.artist : 'Unknown Artist');
album._artistName = artistName;
return `
<div class="card lastfm-card" data-name="${escapeHtml(album.name)}" data-artist="${escapeHtml(artistName)}" style="cursor: pointer;">
<div class="card-image-wrapper">
<img id="${imgId}" src="${image}" class="card-image" loading="lazy" onerror="this.src='/assets/appicon.png'">
@ -347,11 +369,15 @@ export async function loadProfile(username) {
</div>
</div>
`;
}).join('');
})
.join('');
topAlbumsContainer.querySelectorAll('.card').forEach(card => {
topAlbumsContainer.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => handleAlbumClick(card.dataset.name, card.dataset.artist));
card.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
for (const album of albums) {
@ -362,22 +388,26 @@ export async function loadProfile(username) {
}
});
fetchLastFmTopTracks(profile.lastfm_username).then(async tracks => {
fetchLastFmTopTracks(profile.lastfm_username).then(async (tracks) => {
if (tracks.length > 0 && topTracksSection && topTracksContainer) {
topTracksSection.style.display = 'block';
topTracksContainer.innerHTML = tracks.map((track, index) => {
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
topTracksContainer.innerHTML = tracks
.map((track, index) => {
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
const imgId = `top-track-img-${index}`;
track._imgId = imgId;
track._needsCover = !hasImage;
const artistName = track.artist?.name || track.artist?.['#text'] || (typeof track.artist === 'string' ? track.artist : 'Unknown Artist');
track._artistName = artistName;
const imgId = `top-track-img-${index}`;
track._imgId = imgId;
track._needsCover = !hasImage;
return `
const artistName =
track.artist?.name ||
track.artist?.['#text'] ||
(typeof track.artist === 'string' ? track.artist : 'Unknown Artist');
track._artistName = artistName;
return `
<div class="track-item lastfm-track" data-title="${escapeHtml(track.name)}" data-artist="${escapeHtml(artistName)}" style="grid-template-columns: 40px 1fr auto; cursor: pointer;">
<img id="${imgId}" src="${image}" class="track-item-cover" style="width: 40px; height: 40px; border-radius: 4px;" loading="lazy" onerror="this.src='/assets/appicon.png'">
<div class="track-item-info">
@ -389,11 +419,15 @@ export async function loadProfile(username) {
<div class="track-item-duration" style="font-size: 0.8rem; min-width: auto;">${parseInt(track.playcount).toLocaleString()} plays</div>
</div>
`;
}).join('');
})
.join('');
topTracksContainer.querySelectorAll('.track-item').forEach(item => {
topTracksContainer.querySelectorAll('.track-item').forEach((item) => {
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
item.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
for (const track of tracks) {
@ -415,8 +449,8 @@ export async function loadProfile(username) {
if (profile.privacy?.playlists !== 'private' || isOwner) {
const container = document.getElementById('profile-playlists-container');
const playlists = profile.user_playlists || {};
Object.values(playlists).forEach(playlist => {
Object.values(playlists).forEach((playlist) => {
if (!playlist.isPublic && !isOwner) return;
const card = document.createElement('div');
@ -437,21 +471,22 @@ export async function loadProfile(username) {
});
if (container.children.length === 0) {
container.innerHTML = '<p style="color: var(--muted-foreground); grid-column: 1/-1; text-align: center;">No public playlists.</p>';
container.innerHTML =
'<p style="color: var(--muted-foreground); grid-column: 1/-1; text-align: center;">No public playlists.</p>';
}
}
}
export function openEditProfile() {
syncManager.getUserData().then(data => {
syncManager.getUserData().then((data) => {
if (!data || !data.profile) return;
const p = data.profile;
editUsername.value = p.username || '';
editDisplayName.value = p.display_name || '';
resetAvatarControl(p.avatar_url);
resetBannerControl(p.banner);
editStatusJson.value = p.status || '';
editStatusSearch.value = '';
if (p.status) {
@ -476,7 +511,7 @@ export function openEditProfile() {
editAbout.value = p.about || '';
editWebsite.value = p.website || '';
editLastfm.value = p.lastfm_username || '';
privacyPlaylists.checked = p.privacy?.playlists !== 'private';
privacyLastfm.checked = p.privacy?.lastfm !== 'private';
@ -487,7 +522,7 @@ export function openEditProfile() {
async function saveProfile() {
const newUsername = editUsername.value.trim();
if (!newUsername) {
usernameError.textContent = "Username cannot be empty";
usernameError.textContent = 'Username cannot be empty';
usernameError.style.display = 'block';
return;
}
@ -496,7 +531,7 @@ async function saveProfile() {
if (currentUser.profile.username !== newUsername) {
const taken = await syncManager.isUsernameTaken(newUsername);
if (taken) {
usernameError.textContent = "Username is already taken";
usernameError.textContent = 'Username is already taken';
usernameError.style.display = 'block';
return;
}
@ -518,8 +553,8 @@ async function saveProfile() {
lastfm_username: editLastfm.value.trim(),
privacy: {
playlists: privacyPlaylists.checked ? 'public' : 'private',
lastfm: privacyLastfm.checked ? 'public' : 'private'
}
lastfm: privacyLastfm.checked ? 'public' : 'private',
},
};
try {
@ -552,7 +587,7 @@ viewMyProfileBtn.addEventListener('click', async () => {
}
});
authManager.onAuthStateChanged(user => {
authManager.onAuthStateChanged((user) => {
viewMyProfileBtn.style.display = user ? 'inline-block' : 'none';
});
@ -585,18 +620,19 @@ const performStatusSearch = debounce(async (query) => {
try {
const [tracks, albums] = await Promise.all([
api.searchTracks(query, { limit: 3 }),
api.searchAlbums(query, { limit: 3 })
api.searchAlbums(query, { limit: 3 }),
]);
statusSearchResults.innerHTML = '';
const createItem = (item, type) => {
const div = document.createElement('div');
div.className = 'search-result-item';
const title = item.title;
const subtitle = type === 'track' ? (item.artist?.name || 'Unknown Artist') : (item.artist?.name || 'Unknown Artist');
const subtitle =
type === 'track' ? item.artist?.name || 'Unknown Artist' : item.artist?.name || 'Unknown Artist';
const image = api.getCoverUrl(item.album?.cover || item.cover);
div.innerHTML = `
<img src="${image}">
<div class="search-result-info">
@ -604,7 +640,7 @@ const performStatusSearch = debounce(async (query) => {
<div class="search-result-subtitle">${type === 'track' ? 'Song' : 'Album'} ${subtitle}</div>
</div>
`;
div.onclick = () => {
const data = {
type: type,
@ -613,7 +649,7 @@ const performStatusSearch = debounce(async (query) => {
title: title,
subtitle: subtitle,
image: image,
link: `/${type}/${item.id}`
link: `/${type}/${item.id}`,
};
editStatusJson.value = JSON.stringify(data);
showStatusPreview(data);
@ -622,10 +658,10 @@ const performStatusSearch = debounce(async (query) => {
return div;
};
tracks.items.forEach(t => statusSearchResults.appendChild(createItem(t, 'track')));
albums.items.forEach(a => statusSearchResults.appendChild(createItem(a, 'album')));
tracks.items.forEach((t) => statusSearchResults.appendChild(createItem(t, 'track')));
albums.items.forEach((a) => statusSearchResults.appendChild(createItem(a, 'album')));
statusSearchResults.style.display = (tracks.items.length || albums.items.length) ? 'block' : 'none';
statusSearchResults.style.display = tracks.items.length || albums.items.length ? 'block' : 'none';
} catch (e) {
console.error('Status search failed', e);
}
@ -639,7 +675,9 @@ document.addEventListener('click', (e) => {
});
function renderEditFavoriteAlbums() {
editFavoriteAlbumsList.innerHTML = currentFavoriteAlbums.map((album, index) => `
editFavoriteAlbumsList.innerHTML = currentFavoriteAlbums
.map(
(album, index) => `
<div class="edit-favorite-album-item" style="background: var(--secondary); padding: 0.5rem; border-radius: var(--radius); border: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<img src="${api.getCoverUrl(album.cover)}" style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover;">
@ -651,9 +689,11 @@ function renderEditFavoriteAlbums() {
</div>
<textarea class="template-input album-description-input" data-index="${index}" placeholder="Why is this a favorite?" style="min-height: 60px; font-size: 0.85rem; resize: vertical;">${escapeHtml(album.description || '')}</textarea>
</div>
`).join('');
`
)
.join('');
editFavoriteAlbumsList.querySelectorAll('.remove-album-btn').forEach(btn => {
editFavoriteAlbumsList.querySelectorAll('.remove-album-btn').forEach((btn) => {
btn.onclick = () => {
const idx = parseInt(btn.dataset.index);
currentFavoriteAlbums.splice(idx, 1);
@ -661,7 +701,7 @@ function renderEditFavoriteAlbums() {
};
});
editFavoriteAlbumsList.querySelectorAll('.album-description-input').forEach(input => {
editFavoriteAlbumsList.querySelectorAll('.album-description-input').forEach((input) => {
input.oninput = () => {
const idx = parseInt(input.dataset.index);
currentFavoriteAlbums[idx].description = input.value;
@ -670,10 +710,10 @@ function renderEditFavoriteAlbums() {
if (currentFavoriteAlbums.length >= 5) {
editFavoriteAlbumsSearch.disabled = true;
editFavoriteAlbumsSearch.placeholder = "Max 5 albums reached";
editFavoriteAlbumsSearch.placeholder = 'Max 5 albums reached';
} else {
editFavoriteAlbumsSearch.disabled = false;
editFavoriteAlbumsSearch.placeholder = "Search for an album...";
editFavoriteAlbumsSearch.placeholder = 'Search for an album...';
}
}
@ -692,11 +732,11 @@ const performFavoriteAlbumSearch = debounce(async (query) => {
return;
}
results.items.forEach(album => {
results.items.forEach((album) => {
const div = document.createElement('div');
div.className = 'search-result-item';
const image = api.getCoverUrl(album.cover);
div.innerHTML = `
<img src="${image}">
<div class="search-result-info">
@ -704,14 +744,14 @@ const performFavoriteAlbumSearch = debounce(async (query) => {
<div class="search-result-subtitle">${album.artist?.name || 'Unknown Artist'}</div>
</div>
`;
div.onclick = () => {
currentFavoriteAlbums.push({
id: album.id,
title: album.title,
artist: album.artist?.name || 'Unknown Artist',
cover: album.cover,
description: ''
description: '',
});
renderEditFavoriteAlbums();
editFavoriteAlbumsSearch.value = '';
@ -728,27 +768,23 @@ const performFavoriteAlbumSearch = debounce(async (query) => {
editFavoriteAlbumsSearch.addEventListener('input', (e) => performFavoriteAlbumSearch(e.target.value.trim()));
function getLastFmImage(images) {
if (!images) return null;
const imgArray = Array.isArray(images) ? images : [images];
const sizes = ['extralarge', 'large', 'medium', 'small'];
const placeholders = [
'2a96cbd8b46e442fc41c2b86b821562f',
'c6f59c1e5e7240a4c0d427abd71f3dbb'
];
const placeholders = ['2a96cbd8b46e442fc41c2b86b821562f', 'c6f59c1e5e7240a4c0d427abd71f3dbb'];
const isValidUrl = (url) => {
if (!url) return false;
return !placeholders.some(ph => url.includes(ph));
return !placeholders.some((ph) => url.includes(ph));
};
for (const size of sizes) {
const img = imgArray.find(i => i.size === size);
const img = imgArray.find((i) => i.size === size);
if (img && img['#text'] && isValidUrl(img['#text'])) return img['#text'];
}
const anyImg = imgArray.find(i => i['#text'] && isValidUrl(i['#text']));
const anyImg = imgArray.find((i) => i['#text'] && isValidUrl(i['#text']));
if (anyImg) return anyImg['#text'];
return null;
}
@ -801,12 +837,12 @@ async function handleTrackClick(title, artist) {
async function fetchFallbackCover(title, artist, imgId) {
try {
const query = `${title} ${artist}`;
await new Promise(r => setTimeout(r, 100));
await new Promise((r) => setTimeout(r, 100));
const results = await api.searchTracks(query, { limit: 5 });
let foundCover = false;
if (results.items && results.items.length > 0) {
const found = results.items.find(item => item.album?.cover);
const found = results.items.find((item) => item.album?.cover);
if (found) {
const newUrl = api.getCoverUrl(found.album.cover);
const imgEl = document.getElementById(imgId);
@ -828,12 +864,12 @@ async function fetchFallbackCover(title, artist, imgId) {
async function fetchFallbackAlbumCover(title, artist, imgId) {
try {
const query = `${title} ${artist}`;
await new Promise(r => setTimeout(r, 100));
await new Promise((r) => setTimeout(r, 100));
const results = await api.searchAlbums(query, { limit: 5 });
let foundCover = false;
if (results.items && results.items.length > 0) {
const found = results.items.find(item => item.cover);
const found = results.items.find((item) => item.cover);
if (found) {
const newUrl = api.getCoverUrl(found.cover);
const imgEl = document.getElementById(imgId);
@ -854,18 +890,17 @@ async function fetchFallbackAlbumCover(title, artist, imgId) {
async function fetchFallbackArtistImage(artistName, imgId) {
try {
await new Promise(r => setTimeout(r, 100));
await new Promise((r) => setTimeout(r, 100));
const results = await api.searchArtists(artistName, { limit: 3 });
if (results.items && results.items.length > 0) {
const found = results.items.find(item => item.picture);
const found = results.items.find((item) => item.picture);
if (found) {
const newUrl = api.getArtistPictureUrl(found.picture);
const imgEl = document.getElementById(imgId);
if (imgEl) imgEl.src = newUrl;
}
}
} catch (e) {
}
} catch (e) {}
}
async function fetchLastFmRecentTracks(username) {
@ -920,4 +955,4 @@ async function fetchLastFmTopTracks(username) {
console.error('Failed to fetch Last.fm top tracks', e);
return [];
}
}
}

View file

@ -76,7 +76,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
cancelEmailBtn.addEventListener('click', () => {
authModal.classList.remove('active');
});
authModal.querySelector('.modal-overlay').addEventListener('click', () => {
authModal.classList.remove('active');
});

View file

@ -74,26 +74,26 @@ Create two collections: `DB_users` and `public_playlists` (do NOT use the defaul
#### DB_users Fields
| Field Name | Type | Description |
| ------------------- | ---------- | ------------------------- |
| `firebase_id` | Plain Text | Links to Firebase user ID |
| `lastUpdated` | Number | Timestamp of last update |
| `history` | JSON | User listening history |
| `library` | JSON | User's saved library |
| `user_playlists` | JSON | User's custom playlists |
| `user_folders` | JSON | User's playlist folders |
| `deleted_playlists` | JSON | Soft-deleted playlists |
| `username` | Plain Text | Unique username |
| `display_name` | Plain Text | Profile display name |
| `avatar_url` | URL | Profile avatar URL |
| `banner` | URL | Profile banner URL |
| `status` | Plain Text | User status |
| `about` | Plain Text | About me bio |
| `website` | URL | Personal website URL |
| `lastfm_username` | Plain Text | Last.fm username |
| `privacy` | JSON | Privacy settings |
| Field Name | Type | Description |
| --------------------- | --------------- | --------------------------------- |
| `firebase_id` | Plain Text | Links to Firebase user ID |
| `lastUpdated` | Number | Timestamp of last update |
| `history` | JSON | User listening history |
| `library` | JSON | User's saved library |
| `user_playlists` | JSON | User's custom playlists |
| `user_folders` | JSON | User's playlist folders |
| `deleted_playlists` | JSON | Soft-deleted playlists |
| `username` | Plain Text | Unique username |
| `display_name` | Plain Text | Profile display name |
| `avatar_url` | URL | Profile avatar URL |
| `banner` | URL | Profile banner URL |
| `status` | Plain Text | User status |
| `about` | Plain Text | About me bio |
| `website` | URL | Personal website URL |
| `lastfm_username` | Plain Text | Last.fm username |
| `privacy` | JSON | Privacy settings |
| `profile_data_source` | Select (lastfm) | Preferred data source for profile |
| `favorite_albums` | JSON | User's favorite albums |
| `favorite_albums` | JSON | User's favorite albums |
#### public_playlists Fields

View file

@ -7233,8 +7233,8 @@ textarea:focus {
top: 0;
left: 0;
z-index: 0;
mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 50%, rgba(0,0,0,0));
-webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 50%, rgba(0,0,0,0));
mask-image: linear-gradient(to bottom, rgb(0, 0, 0, 1) 50%, rgb(0, 0, 0, 0));
mask-image: linear-gradient(to bottom, rgb(0, 0, 0, 1) 50%, rgb(0, 0, 0, 0));
}
.profile-info-section {
@ -7272,16 +7272,16 @@ textarea:focus {
#profile-display-name {
font-size: 3rem;
font-weight: 800;
margin: 0 0 0.5rem 0;
margin: 0 0 0.5rem;
line-height: 1.1;
text-shadow: 0 4px 12px rgba(0,0,0,0.5);
text-shadow: 0 4px 12px rgb(0, 0, 0, 0.5);
}
.profile-username {
color: var(--muted-foreground);
font-size: 1.2rem;
margin-bottom: 1rem;
background: rgba(0,0,0,0.3);
background: rgb(0, 0, 0, 0.3);
padding: 0.25rem 0.75rem;
border-radius: var(--radius-full);
backdrop-filter: blur(4px);