PageSourceSearch

https://animeslayer.to/assets/js/app.js

js animeslayer.to collected 2026-10-02 17:52:45 UTC 22,499 bytes, 468 lines download raw bytes

1// ── CONFIG ──────────────────────────
2const API_BASE   = '/api/search.php';
3const BROWSE_URL = '/browse';
4// ────────────────────────────────────
5
6// ── Search Modal ─────────────────────────────────────────────────────────────
7
8function mapItem(a) {
9  return {
10    title:    a.title    || a.anime_name            || 'Untitled',
11    image:    a.image    || a.anime_cover_image_url || null,
12    href:     a.href     || a.url                   || '#',
13    type:     a.type     || a.anime_type            || '',
14    episodes: a.episodes != null ? a.episodes : (a.anime_episodes_count != null ? a.anime_episodes_count : null),
15    year:     a.season   || a.anime_release_year    || '',
16    genres:   Array.isArray(a.genres) ? a.genres.join(' • ') : (a.genres || ''),
17  };
18}
19
20function buildResultsHTML(items, q) {
21  if (!items.length) {
22    return `<div class="s-state">لا توجد نتائج لـ "<strong>${q}</strong>"</div>`;
23  }
24  let html = '';
25  items.forEach((raw, i) => {
26    const a = mapItem(raw);
27    const parts = [a.type, a.episodes != null ? a.episodes + ' eps' : null, a.year].filter(
27Boolean);
28    const meta  = parts.map((p, pi) => pi === 0 ? `<span>${p}</span>` : `<span class="dot"></span><span>${p}</span>`).join('');
29    const genres = a.genres ? `<div class="s-genres">${a.genres}</div>` : '';
30    const thumb  = a.image  ? `<img src="${a.image}" alt="${a.title}" loading="lazy">` : '';
31    html += `
32      ${i > 0 ? '<div class="s-divider"></div>' : ''}
33      <a class="s-item" href="${a.href}">
34        <div class="s-bg">${a.image ? `<img src="${a.image}" alt="" loading="lazy">` : ''}<div class="s-overlay"></div></div>
35        <div class="s-content">
36          <div class="s-thumb">${thumb}</div>
37          <div class="s-info">
38            <div class="s-title">${a.title}</div>
39            <div class="s-meta">${meta}</div>
40            ${genres}
41          </div>
42        </div>
43      </a>`;
44  });
45  html += `<div class="s-footer"><a href="${BROWSE_URL}?keyword=${encodeURIComponent(q)}">عرض كل النتائج →</a></div>`;
46  return html;
47}
48
49function injectSearchStyles() {
50  if (document.getElementById('search-styles')) return;
51  const style = document.createElement('style');
52  style.id = 'search-styles';
53  style.textContent = `
54    #bits-c16 .s-state{padding:24px;text-align:center;font-size:13px;color:rgba(255,255,255,0.4);display:flex;justify-content:center}
55    #bits-c16 .s-item{position:relative;display:flex;align-items:center;gap:12px;padding:10px 14px;text-decoration:none;color:inherit;overflow:hidden;border-left:3px solid transparent;transition:border-color .25s}
56    #bits-c16 .s-item:hover{border-left-color:rgba(255,255,255,0.4)}
57    #bits-c16 .s-bg{position:absolute;inset:0;z-index:0}
58    #bits-c16 .s-bg img{width:100%;height:100%;object-fit:cover;filter:blur(4px) grayscale(1);transform:scale(1.05);transition:filter .4s,transform .4s}
59    #bits-c16 .s-item:hover .s-bg img{filter:blur(4px) grayscale(0);transform:scale(1.1)}
60    #bits-c16 .s-overlay{position:absolute;inset:0;background:rgba(0,0,0,0.85);transition:background .3s}
61    #bits-c16 .s-item:hover .s-overlay{background:rgba(0,0,0,0.62)}
62    #bits-c16 .s-content{position:relative;z-index:1;display:flex;align-items:center;gap:12px;width:100%}
63    #bits-c16 .s-thumb{flex-shrink:0;width:40px;height:56px;border-radius:5px;overflow:hidden;border:1px solid rgba(255,255,255,0.1)}
64    #bits-c16 .s-item:hover .s-thumb{border-color:rgba(255,255,255,0.3)}
65    #bits-c16 .s-thumb img{width:100%;height:100%;object-fit:cover;display:block}
66    #bits-c16 .s-info{flex:1;min-width:0}
67    #bits-c16 .s-title{font-size:13px;font-weight:500;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
68    #bits-c16 .s-meta{margin-top:3px;font-size:11px;color:rgba(255,255,255,0.6);display:flex;align-items:center;gap:5px}
69    #bits-c16 .s-meta .dot{width:3px;height:3px;border-radius:50%;background:rgba(255,255,255,0.35)}
70    #bits-c16 .s-genres{margin-top:3px;font-size:11px;color:rgba(255,255,255,0.45);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
71    #bits-c16 .s-divider{margin:0 14px;height:1px;background:rgba(255,255,255,0.07)}
72    #bits-c16 .s-footer{padding:10px 14px;border-top:1px solid rgba(255,255,255,0.07);text-align:center}
73    #bits-c16 .s-footer a{font-size:12px;color:rgba(255,255,255,0.4);text-decoration:none}
74    #bits-c16 .s-footer a:hover{color:#fff}
75    #bits-c16 .s-dots{display:flex;gap:5px;padding:24px;justify-content:center}
76    #bits-c16 .s-dots span{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,0.5);animation:sdot 1s infinite ease-in-out}
77    #bits-c16 .s-dots span:nth-child(2){animation-delay:.15s}
78    #bits-c16 .s-dots span:nth-child(3){animation-delay:.3s}
79    @keyframes sdot{0%,80%,100%{transform:translateY(0);opacity:.3}40%{transform:translateY(-5px);opacity:1}}
80  `;
81  document.head.appendChild(style);
82}
83
84function attachSearchLogic() {
85  const list  = document.getElementById('bits-c16');
86  const input = document.getElementById('search-input');
87  if (!list || !input) return;
88
89  let debounce = null, lastQ = '';
90
91  input.addEventListener('input', () => {
92    const q = input.value.trim();
93    clearTimeout(debounce);
94    if (!q) { list.innerHTML = ''; lastQ = ''; return; }
95    debounce = setTimeout(async () => {
96      if (q === lastQ) return;
97      lastQ = q;
98      list.innerHTML = '<div class="s-dots"><span></span><span></span><span></span></div>';
99      try {
100        const res   = await fetch(`${API_BASE}?q=${encodeURIComponent(q)}`);
101        if (!res.ok) throw new Error();
102        const data  = await res.json();
103        const items = Array.isArray(data) ? data : (data.results || data.data || []);
104        list.innerHTML = buildResultsHTML(items, q);
105      } catch {
106        list.innerHTML = '<div class="s-state">حدث خطأ، حاول مرة أخرى.</div>';
107      }
108    }, 300);
109  });
110
111  input.addEventListener('keydown', (e) => {
112    if (e.key === 'Enter') {
113      const q = input.value.trim();
114      if (q) window.location.href = `${BROWSE_URL}?keyword=${encodeURIComponent(q)}`;
115    }
116  });
117}
118
119const createSearchModal = () => {
120  if (document.getElementById('dynamic-search-modal')) return; // prevent duplicates
121
122  const modalContainer = document.createElement('div');
123  modalContainer.id = 'dynamic-search-modal';
124  modalContainer.innerHTML = `
125    <div class="fixed inset-0 z-[99999] flex items-center justify-center p-4" role="presentation" style="opacity:0;transition:opacity 0.3s ease;">
126      <div class="modal-overlay--lines no-scale fixed inset-0 bg-black/60" aria-label="Close search" id="overlay-close"></div>
127      <div class="relative w-full max-w-2xl" id="search-content" style="transform:scale(0.95) translateY(20px);transition:transform 0.3s ease,opacity 0.3s ease;opacity:0;">
128        <div class="relative mx-auto flex max-w-[600px] flex-col overflow-hidden rounded" id="bits-c13" role="application" data-command-root="" tabindex="-1">
129          <label id="bits-c14" data-command-input-label="" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;transform:translateX(-100%);"></label>
130          <div class="flex justify-between px-2 py-1">
131            <div class="flex items-center gap-2">
132              <p class="text-sm font-medium text-white/90">Quick access:</p>
133              <span class="text-xs font-bold text-white">CTRL</span>
134              <span class="text-white/60">+</span>
135              <span class="text-xs font-bold text-white">S</span>
136            </div>
137          </div>
138          <input class="inline-flex w-full truncate border border-white/15 bg-[#0a0a0a] px-4 py-4 text-base font-medium text-white transition-all placeholder:text-muted-foreground focus:outline-none" placeholder="Search Anime..." autocomplete="off" id="search-input" type="text" data-command-input="" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-labelledby="bits-c14">
139          <div class="flex max-h-[50dvh] flex-col overflow-y-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" id="bits-c16" role="listbox" aria-label="Suggestions..." data-command-list="" style="background-color:rgb(10,10,10);"></div>
140        </div>
141      </div>
142    </div>
143  `;
144
145  document.body.appendChild(modalContainer);
146  document.body.style.overflow = 'hidden';
147
148  const wrapper       = modalContainer.querySelector('[role="presentation"]');
149  const input         = modalContainer.querySelector('#search-input');
150  const searchContent = modalContainer.querySelector('#search-content');
151  const overlayDiv    = modalContainer.querySelector('#overlay-close');
152
153  setTimeout(() => {
154    wrapper.style.opacity = '1';
155    searchContent.style.opacity = '1';
156    searchContent.style.transform = 'scale(1) translateY(0)';
157  }, 10);
158
159  setTimeout(() => input.focus(), 100);
160
161  injectSearchStyles();
162  setTimeout(attachSearchLogic, 50);
163
164  const destroyModal = () => {
165    wrapper.style.opacity = '0';
166    searchContent.style.opacity = '0';
167    searchContent.style.transform = 'scale(0.95) translateY(20px)';
168    setTimeout(() => {
169      modalContainer.remove();
170      document.body.style.overflow = '';
171    }, 300);
172  };
173
174  overlayDiv.addEventListener('click', (e) => {
175    e.stopPropagation();
176    destroyModal();
177  });
178
179  const escHandler = (e) => {
180    if (e.key === 'Escape') {
181      destroyModal();
182      window.removeEventListener('keydown', escHandler);
183    }
184  };
185  window.addEventListener('keydown', escHandler);
186};
187
188// Open modal on search button click
189const searchButton = document.querySelector('button[aria-label="Open search"]');
190searchButton?.addEventListener('click', createSearchModal);
191
192// Ctrl+S shortcut
193window.addEventListener('keydown', (e) => {
194  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
195    e.preventDefault();
196    createSearchModal();
197  }
198});
199
200
201// ── Desktop Sidebar Toggle (RTL) ──────────────────────────────────────────────
202
203const desktopMenuButton = document.querySelector('header button[aria-label="Open menu"]');
204const desktopSidebar    = document.querySelector('.fixed.top-0.left-0.z-\\[70\\]');
205const desktopCloseButton = desktopSidebar?.querySelector('button[aria-label="Close sidebar"]');
206
207if (desktopSidebar) {
208  desktopSidebar.style.transition = 'none';
209  desktopSidebar.classList.remove('left-0', '-translate-x-full');
210  desktopSidebar.classList.add('right-0', 'translate-x-full');
211  setTimeout(() => { desktopSidebar.style.transition = ''; }, 50);
212}
213
214if (desktopMenuButton && desktopSidebar) {
215  desktopMenuButton.addEventListener('click', () => {
216    desktopSidebar.classList.remove('translate-x-full');
217    desktopSidebar.setAttribute('aria-hidden', 'false');
218  });
219
220  if (desktopCloseButton) {
221    desktopCloseButton.addEventListener('click', () => {
222      desktopSidebar.classList.add('translate-x-full');
223      desktopSidebar.setAttribute('aria-hidden', 'true');
224    });
225  }
226}
227
228// Close desktop sidebar when clicking outside
229document.addEventListener('click', (e) => {
230  if (
231    desktopSidebar &&
232    !desktopSidebar.contains(e.target) &&
233    !desktopMenuButton?.contains(e.target) &&
234    !desktopSidebar.classList.contains('translate-x-full')
235  ) {
236    desktopSidebar.classList.add('translate-x-full');
237    desktopSidebar.setAttribute('aria-hidden', 'true');
238  }
239});
240
241if (desktopSidebar) {
242  desktopSidebar.addEventListener('click', (e) => e.stopPropagation());
243}
244
245
246// ── Mobile "More" Menu ────────────────────────────────────────────────────────
247
248const mobileBottomNav = document.querySelector('.fixed.right-0.bottom-0.left-0.z-50');
249const mobileNavButtons = mobileBottomNav?.querySelectorAll('.nav-item');
250
251let mobileMoreButton = null;
252if (mobileNavButtons) {
253  mobileNavButtons.forEach((button) => {
254    if (button.querySelector('span')?.textContent?.trim() === 'More') {
255      mobileMoreButton = button;
256    }
257  });
258}
259
260if (mobileMoreButton && mobileBottomNav) {
261  let isOpen = false;
262  let mobileDropdown = null;
263
264  mobileMoreButton.addEventListener('click', (e) => {
265    e.stopPropagation();
266    isOpen = !isOpen;
267
268    if (isOpen) {
269      mobileDropdown = document.createElement('div');
270      mobileDropdown.style.cssText = `
271        position:fixed;bottom:80px;left:12px;right:12px;z-index:60;
272        opacity:0;transform:translateY(20px);
273        transition:opacity 0.3s ease,transform 0.3s ease;
274      `;
275      mobileDropdown.innerHTML = `
276        <div style="overflow:hidden;border-radius:1rem;border:1px solid rgba(255,255,255,0.1);background:#000;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);backdrop-filter:blur(12px);">
277          <div style="display:flex;flex-direction:column;gap:4px;padding:8px;">
278            <a href="#torrents" style="display:flex;align-items:center;gap:12px;border-radius:12px;padding:12px 16px;color:#9ca3af;text-decoration:none;transition:all 0.2s;">
279              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 15V3"></path><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><path d="m7 10 5 5 5-5"></path></svg>
280              <span style="font-size:14px;font-weight:500;">Torrents</span>
281            </a>
282            <a href="#/community/forums" style="display:flex;align-items:center;gap:12px;border-radius:12px;padding:12px 16px;color:#9ca3af;text-decoration:none;transition:all 0.2s;">
283              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 21a8 8 0 0 0-16 0"></path><circle cx="10" cy="8" r="5"></circle><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"></path></svg>
284              <span style="font-size:14px;font-weight:500;">Forums</span>
285            </a>
286            <a href="#/community/w2g" style="display:flex;align-items:center;gap:12px;border-radius:12px;padding:12px 16px;color:#9ca3af;text-decoration:none;transition:all 0.2s;">
287              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 21a8 8 0 0 0-16 0"></path><circle cx="10" cy="8" r="5"></circle><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"></path></svg>
288              <span style="font-size:14px;font-weight:500;">Watch Together</span>
289            </a>
290            <a href="#/music" style="display:flex;align-items:center;gap:12px;border-radius:12px;padding:12px 16px;color:#9ca3af;text-decoration:none;transition:all 0.2s;">
291              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
292              <span style="font-size:14px;font-weight:500;">Music</span>
293            </a>
294            <a href="#/schedule" style="display:flex;align-items:center;gap:12px;border-radius:12px;padding:12px 16px;color:#9ca3af;text-decoration:none;transition:all 0.2s;">
295              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>
296              <span style="font-size:14px;font-weight:500;">Schedule</span>
297            </a>
298          </div>
299        </div>
300      `;
301
302      const links = mobileDropdown.querySelectorAll('a');
303      links.forEach(link => {
304        link.addEventListener('mouseenter', function() { this.style.background = 'rgba(255,255,255,0.05)'; this.style.color = '#e5e7eb'; });
305        link.addEventListener('mouseleave', function() { this.style.background = ''; this.style.color = '#9ca3af'; });
306      });
307
308      document.body.appendChild(mobileDropdown);
309      setTimeout(() => {
310        mobileDropdown.style.opacity = '1';
311        mobileDropdown.style.transform = 'translateY(0)';
312      }, 10);
313
314      const iconContainer = mobileMoreButton.querySelector('.icon-container');
315      if (iconContainer) iconContainer.style.transform = 'scale(1.1) rotate(90deg)';
316
317    } else {
318      closeMobileDropdown();
319    }
320  });
321
322  function closeMobileDropdown() {
323    if (mobileDropdown) {
324      mobileDropdown.style.opacity = '0';
325      mobileDropdown.style.transform = 'translateY(20px)';
326      setTimeout(() => { mobileDropdown?.remove(); mobileDropdown = null; }, 300);
327    }
328    const iconContainer = mobileMoreButton.querySelector('.icon-container');
329    if (iconContainer) iconContainer.style.transform = 'scale(1) rotate(0deg)';
330    isOpen = false;
331  }
332
333  document.addEventListener('click', (e) => {
334    if (mobileDropdown && !mobileDropdown.contains(e.target) && !mobileMoreButton.contains(e.target)) {
335      closeMobileDropdown();
336    }
337  });
338}
339
340
341// ── Header scroll effect ──────────────────────────────────────────────────────
342
343const header = document.querySelector('header');
344window.addEventListener('scroll', () => {
345  if (window.scrollY > 10) {
346    header.classList.add('bg-black/20', 'backdrop-blur-md');
347    header.classList.remove('bg-transparent');
348  } else {
349    header.classList.remove('bg-black/20', 'backdrop-blur-md');
350    header.classList.add('bg-transparent');
351  }
352});
353
354
355// ── Swiper initialization ─────────────────────────────────────────────────────
356
357document.addEventListener('DOMContentLoaded', () => {
358  const swiperEls = document.querySelectorAll('swiper-container');
359
360  swiperEls.forEach((swiperEl) => {
361    const wrapper = swiperEl.closest('.group\\/swiper');
362    const prevBtn = wrapper?.querySelector('[aria-label="Previous slide"]');
363    const nextBtn = wrapper?.querySelector('[aria-label="Next slide"]');
364
365    Object.assign(swiperEl, {
366      slidesPerView: 'auto',
367      spaceBetween: 12,
368      freeMode: true,
369      grabCursor: true,
370      rtl: true,
371      mousewheel: { forceToAxis: true },
372      on: {
373        slideChange(swiper)    { updateButtons(swiper, prevBtn, nextBtn); },
374        reachBeginning(swiper) { updateButtons(swiper, prevBtn, nextBtn); },
375        reachEnd(swiper)       { updateButtons(swiper, prevBtn, nextBtn); },
376      },
377    });
378
379    swiperEl.initialize();
380    prevBtn?.addEventListener('click', () => swiperEl.swiper.slidePrev());
381    nextBtn?.addEventListener('click', () => swiperEl.swiper.slideNext());
382    setTimeout(() => updateButtons(swiperEl.swiper, prevBtn, nextBtn), 100);
383  });
384});
385
386function updateButtons(swiper, prevBtn, nextBtn) {
387  if (!swiper || !prevBtn || !nextBtn) return;
388  prevBtn.disabled = swiper.isBeginning;
389  prevBtn.setAttribute('aria-disabled', swiper.isBeginning);
390  nextBtn.disabled = swiper.isEnd;
391  nextBtn.setAttribute('aria-disabled', swiper.isEnd);
392}
393
394
395// ── Inline search bar (non-modal) ─────────────────────────────────────────────
396// Kept separate from the modal search — targets a different input on the page.
397
398(function () {
399  const input = document.querySelector('input[aria-label="Search anime"]');
400  const btn   = document.querySelector('button[aria-label="Search"]');
401  const wrap  = input?.closest('.relative.w-full');
402
403  if (!input || !wrap) return;
404
405  const dropdown = document.createElement('div');
406  dropdown.className = 'search-dropdown';
407  wrap.appendChild(dropdown);
408
409  let debounce = null, lastQ = '';
410
411  function open()  { dropdown.classList.add('open'); }
412  function close() { dropdown.classList.remove('open'); }
413
414  function setState(type, q) {
415    dropdown.innerHTML = '';
416    const d = document.createElement('div');
417    d.className = 's-state';
418    if (type === 'loading') {
419      d.innerHTML = '<div class="s-dots"><span></span><span></span><span></span></div>';
420    } else if (type === 'empty') {
421      d.innerHTML = 'لا توجد نتائج لـ "<strong>' + q + '</strong>"';
422    } else if (type === 'error') {
423      d.innerHTML = 'حدث خطأ، حاول مرة أخرى.';
424    }
425    dropdown.appendChild(d);
426    open();
427  }
428
429  function render(items, q) {
430    dropdown.innerHTML = buildResultsHTML(items, q);
431    open();
432  }
433
434  async function fetchResults(q) {
435    if (q === lastQ) return;
436    lastQ = q;
437    setState('loading');
438    try {
439      const res   = await fetch(`${API_BASE}?q=${encodeURIComponent(q)}`);
440      if (!res.ok) throw new Error();
441      const data  = await res.json();
442      const items = Array.isArray(data) ? data : (data.results || data.data || []);
443      if (!items.length) { setState('empty', q); return; }
444      render(items, q);
445    } catch { setState('error'); }
446  }
447
448  input.addEventListener('input', () => {
449    const q = input.value.trim();
450    clearTimeout(debounce);
451    if (!q) { close(); lastQ = ''; return; }
452    debounce = setTimeout(() => fetchResults(q), 300);
453  });
454
455  btn?.addEventListener('click', () => {
456    const q = input.value.trim();
457    if (q) { lastQ = ''; fetchResults(q); }
458  });
459
460  input.addEventListener('keydown', (e) => {
461    if (e.key === 'Enter') { const q = input.value.trim(); if (q) { lastQ = ''; fetchResults(q); } }
462    if (e.key === 'Escape') close();
463  });
464
465  document.addEventListener('click', (e) => {
466    if (!wrap.contains(e.target)) close();
467  });
468})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.