faster smaller

This commit is contained in:
Andrey Kondratev
2025-11-09 18:48:57 +05:00
parent ca27a2b3f0
commit 6db48b16a7
11 changed files with 470 additions and 76 deletions

1
public/index.min.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,6 @@
# Allow search engines
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Yandexbot
Allow: /
# Allow all search engines and bots
User-agent: *
Disallow: /
Allow: /
# Sitemap
Sitemap: https://music.quixy.uk/sitemap.xml

View File

@@ -46,6 +46,8 @@ class QuixoticApp {
private welcomePlaceholder!: HTMLElement;
private searchTimeout?: NodeJS.Timeout;
private currentVideos: VideoResult[] = [];
private loadingStartTime: number = 0;
private minLoadingDuration: number = 400; // Минимальное время показа спиннера (ms)
constructor() {
this.tg = (window as WindowWithTelegram).Telegram?.WebApp;
@@ -91,10 +93,13 @@ class QuixoticApp {
return;
}
// Set new timeout for search (500ms delay)
// Show loading spinner immediately for better UX feedback
this.showLoading();
// Set new timeout for search (300ms delay - best practice for instant search)
this.searchTimeout = setTimeout(() => {
this.search();
}, 500);
}, 300);
});
// Still handle Enter key for immediate search
@@ -148,14 +153,35 @@ class QuixoticApp {
}
const data: SearchResponse = await response.json();
// Ensure minimum loading time for better UX (prevents flashing)
const elapsedTime = Date.now() - this.loadingStartTime;
const remainingTime = Math.max(0, this.minLoadingDuration - elapsedTime);
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
this.displayResults(data.videos);
} catch (error) {
console.error('Search error:', error);
// Ensure minimum loading time even for errors
const elapsedTime = Date.now() - this.loadingStartTime;
const remainingTime = Math.max(0, this.minLoadingDuration - elapsedTime);
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
this.showNoResults();
}
}
private showLoading(): void {
// Record loading start time
this.loadingStartTime = Date.now();
// Clear any existing status messages
const existingMessage = document.querySelector('.tg-status-message');
if (existingMessage) {
@@ -201,8 +227,15 @@ class QuixoticApp {
this.noResults.classList.add('tg-hidden');
this.noResults.style.display = 'none';
this.results.innerHTML = videos.map(video => `
<div class='tg-list-item' onclick='app.convertVideo("${video.id}", "${this.escapeHtml(video.title)}", "${this.escapeHtml(video.url)}")'>
// Use DocumentFragment for better performance
const fragment = document.createDocumentFragment();
videos.forEach(video => {
const item = document.createElement('div');
item.className = 'tg-list-item';
item.dataset.videoId = video.id;
item.innerHTML = `
<div class='tg-list-item__content'>
<div class='tg-list-item__media'>
<img class='tg-list-item__thumbnail'
@@ -217,29 +250,35 @@ class QuixoticApp {
<div class='tg-list-item__subtitle'>${this.escapeHtml(video.channel)}</div>
</div>
</div>
</div>
`).join('');
// Add touch event listeners to prevent sticky states
this.results.querySelectorAll('.tg-list-item').forEach(item => {
const element = item as HTMLElement;
`;
// Use event listener instead of inline onclick
item.addEventListener('click', () => {
this.convertVideo(video.id, video.title, video.url);
});
// Reset visual state on touch end
element.addEventListener('touchend', () => {
item.addEventListener('touchend', () => {
setTimeout(() => {
element.blur();
element.style.background = '';
element.style.transform = '';
item.blur();
item.style.background = '';
item.style.transform = '';
}, 100);
});
}, { passive: true });
// Also handle mouse leave for desktop
element.addEventListener('mouseleave', () => {
element.style.background = '';
element.style.transform = '';
item.addEventListener('mouseleave', () => {
item.style.background = '';
item.style.transform = '';
});
fragment.appendChild(item);
});
// Clear and append once
this.results.innerHTML = '';
this.results.appendChild(fragment);
// Lazy load images with IntersectionObserver
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@@ -281,8 +320,8 @@ class QuixoticApp {
const performer = video?.channel || 'Unknown Artist';
const thumbnail = video?.thumbnail || '';
// Find the clicked element by looking for the one that contains this videoId
const videoElement = document.querySelector(`[onclick*="${videoId}"]`) as HTMLElement;
// Find the clicked element using data attribute
const videoElement = document.querySelector(`[data-video-id="${videoId}"]`) as HTMLElement;
if (videoElement) {
// Force blur to reset any active/focus states
videoElement.blur();

10
public/sitemap.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://music.quixy.uk/</loc>
<lastmod>2025-11-07</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
</urlset>

View File

@@ -203,25 +203,29 @@ body {
padding: var(--tg-spacing-xxl);
display: none;
z-index: 10;
opacity: 0;
transition: opacity 0.15s ease-in;
}
.tg-spinner.tg-spinner--visible {
display: block;
animation: tg-fade-in 0.15s ease-in forwards;
}
.tg-spinner__icon {
width: 32px;
height: 32px;
border: 2px solid var(--tg-color-secondary-bg);
border-top: 2px solid var(--tg-color-button);
width: 40px;
height: 40px;
border: 3px solid var(--tg-color-secondary-bg);
border-top: 3px solid var(--tg-color-button);
border-radius: 50%;
margin: 0 auto var(--tg-spacing-lg);
animation: tg-spin 1s linear infinite;
animation: tg-spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.tg-spinner__text {
font-size: var(--tg-font-size-sm);
color: var(--tg-color-hint);
font-weight: 500;
}
@keyframes tg-spin {
@@ -229,6 +233,17 @@ body {
100% { transform: rotate(360deg); }
}
@keyframes tg-fade-in {
from {
opacity: 0;
transform: translate(-50%, -45%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
/* List component */
.tg-list {
display: none;