diff --git a/.gitignore b/.gitignore
index 3f0844a..1da77d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,5 @@ Thumbs.db
# Build
dist/
-build/
\ No newline at end of file
+build/
+!public
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..aef8223
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ Quixotic Music
+
+
+
+
+
+
+ <µ Quixotic
+ 0948 8 A:0G09
+
+
+
+
+
+
+
+
+
+
+
+
8G53> =5 =0945=>. >?@>1C9B5 4@C3>9 70?@>A.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/script.js b/public/script.js
new file mode 100644
index 0000000..fd4e686
--- /dev/null
+++ b/public/script.js
@@ -0,0 +1,161 @@
+class QuixoticApp {
+ constructor() {
+ this.tg = window.Telegram?.WebApp;
+ this.init();
+ this.bindEvents();
+ }
+
+ init() {
+ if (this.tg) {
+ this.tg.ready();
+ this.tg.expand();
+ this.tg.MainButton.hide();
+ }
+
+ this.searchInput = document.getElementById('searchInput');
+ this.searchBtn = document.getElementById('searchBtn');
+ this.loading = document.getElementById('loading');
+ this.results = document.getElementById('results');
+ this.noResults = document.getElementById('noResults');
+ }
+
+ bindEvents() {
+ this.searchBtn.addEventListener('click', () => this.search());
+ this.searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ this.search();
+ }
+ });
+ }
+
+ async search() {
+ const query = this.searchInput.value.trim();
+ if (!query) return;
+
+ this.showLoading();
+
+ try {
+ const response = await fetch('/api/search', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query,
+ userId: this.tg?.initDataUnsafe?.user?.id || 'demo'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Search failed');
+ }
+
+ const data = await response.json();
+ this.displayResults(data.videos);
+ } catch (error) {
+ console.error('Search error:', error);
+ this.showNoResults();
+ }
+ }
+
+ showLoading() {
+ this.loading.classList.remove('hidden');
+ this.results.classList.add('hidden');
+ this.noResults.classList.add('hidden');
+ this.searchBtn.disabled = true;
+ }
+
+ hideLoading() {
+ this.loading.classList.add('hidden');
+ this.searchBtn.disabled = false;
+ }
+
+ displayResults(videos) {
+ this.hideLoading();
+
+ if (!videos || videos.length === 0) {
+ this.showNoResults();
+ return;
+ }
+
+ this.results.innerHTML = videos.map(video => `
+
+

+
+
${this.escapeHtml(video.title)}
+
${this.escapeHtml(video.channel)}
+
${this.formatDuration(video.duration)}
+
+
+ `).join('');
+
+ this.results.classList.remove('hidden');
+ this.noResults.classList.add('hidden');
+ }
+
+ showNoResults() {
+ this.hideLoading();
+ this.results.classList.add('hidden');
+ this.noResults.classList.remove('hidden');
+ }
+
+ async convertVideo(videoId, title) {
+ const videoElement = event.currentTarget;
+ videoElement.classList.add('converting');
+
+ try {
+ const response = await fetch('/api/convert', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ videoId,
+ title,
+ userId: this.tg?.initDataUnsafe?.user?.id || 'demo'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Conversion failed');
+ }
+
+ const data = await response.json();
+
+ if (this.tg) {
+ this.tg.sendData(JSON.stringify({
+ action: 'send_audio',
+ audioUrl: data.audioUrl,
+ title: title
+ }));
+ } else {
+ // Fallback for testing without Telegram
+ window.open(data.audioUrl, '_blank');
+ }
+ } catch (error) {
+ console.error('Conversion error:', error);
+ if (this.tg) {
+ this.tg.showAlert('H81:0 ?@8 :>=25@B0F88. >?@>1C9B5 5I5 @07.');
+ } else {
+ alert('H81:0 ?@8 :>=25@B0F88. >?@>1C9B5 5I5 @07.');
+ }
+ } finally {
+ videoElement.classList.remove('converting');
+ }
+ }
+
+ formatDuration(seconds) {
+ if (!seconds) return '';
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+}
+
+const app = new QuixoticApp();
\ No newline at end of file
diff --git a/public/style.css b/public/style.css
new file mode 100644
index 0000000..a3d4bdc
--- /dev/null
+++ b/public/style.css
@@ -0,0 +1,206 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ background: var(--tg-theme-bg-color, #f0f0f0);
+ color: var(--tg-theme-text-color, #000);
+ line-height: 1.6;
+}
+
+.container {
+ max-width: 100vw;
+ margin: 0 auto;
+ padding: 20px 16px;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+header h1 {
+ font-size: 2rem;
+ margin-bottom: 8px;
+ color: var(--tg-theme-button-color, #007AFF);
+}
+
+header p {
+ color: var(--tg-theme-hint-color, #666);
+ font-size: 0.9rem;
+}
+
+.search-section {
+ margin-bottom: 30px;
+}
+
+.search-container {
+ display: flex;
+ gap: 10px;
+ background: var(--tg-theme-secondary-bg-color, #fff);
+ border-radius: 12px;
+ padding: 4px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+#searchInput {
+ flex: 1;
+ padding: 12px 16px;
+ border: none;
+ background: transparent;
+ font-size: 1rem;
+ color: var(--tg-theme-text-color, #000);
+ outline: none;
+}
+
+#searchInput::placeholder {
+ color: var(--tg-theme-hint-color, #999);
+}
+
+#searchBtn {
+ padding: 12px 16px;
+ background: var(--tg-theme-button-color, #007AFF);
+ color: var(--tg-theme-button-text-color, #fff);
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: opacity 0.2s;
+}
+
+#searchBtn:hover {
+ opacity: 0.8;
+}
+
+#searchBtn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.loading {
+ text-align: center;
+ padding: 40px 20px;
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--tg-theme-hint-color, #ddd);
+ border-top: 3px solid var(--tg-theme-button-color, #007AFF);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 16px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.results-container {
+ display: grid;
+ gap: 16px;
+}
+
+.video-item {
+ display: flex;
+ background: var(--tg-theme-secondary-bg-color, #fff);
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s, box-shadow 0.2s;
+ cursor: pointer;
+}
+
+.video-item:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+.video-thumbnail {
+ width: 120px;
+ height: 90px;
+ object-fit: cover;
+ flex-shrink: 0;
+}
+
+.video-info {
+ padding: 16px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.video-title {
+ font-weight: 600;
+ font-size: 0.95rem;
+ line-height: 1.4;
+ margin-bottom: 8px;
+ color: var(--tg-theme-text-color, #000);
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.video-channel {
+ font-size: 0.85rem;
+ color: var(--tg-theme-hint-color, #666);
+ margin-bottom: 4px;
+}
+
+.video-duration {
+ font-size: 0.8rem;
+ color: var(--tg-theme-hint-color, #888);
+ background: rgba(0, 0, 0, 0.1);
+ padding: 2px 6px;
+ border-radius: 4px;
+ align-self: flex-start;
+}
+
+.no-results {
+ text-align: center;
+ padding: 60px 20px;
+ color: var(--tg-theme-hint-color, #666);
+}
+
+.hidden {
+ display: none !important;
+}
+
+.converting {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.converting::after {
+ content: ">=25@B0F8O...";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: var(--tg-theme-button-color, #007AFF);
+ color: var(--tg-theme-button-text-color, #fff);
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 0.8rem;
+}
+
+@media (max-width: 480px) {
+ .video-item {
+ flex-direction: column;
+ }
+
+ .video-thumbnail {
+ width: 100%;
+ height: 180px;
+ }
+
+ .container {
+ padding: 16px 12px;
+ }
+}
\ No newline at end of file