mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 20:53:45 -07:00
search pt 1
This commit is contained in:
parent
007ec1856f
commit
8c634a381a
3 changed files with 174 additions and 115 deletions
|
|
@ -6,69 +6,73 @@ interface Props {
|
||||||
const { currentSlugs = [] } = Astro.props;
|
const { currentSlugs = [] } = Astro.props;
|
||||||
|
|
||||||
import { availableLanguages, languageMap } from '../lib/api';
|
import { availableLanguages, languageMap } from '../lib/api';
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ value: 'en', label: 'English' },
|
||||||
|
{ value: 'ja', label: 'Japanese' },
|
||||||
|
{ value: 'ko', label: 'Korean' },
|
||||||
|
{ value: 'zh', label: 'Chinese' },
|
||||||
|
{ value: 'fr', label: 'French' },
|
||||||
|
{ value: 'es', label: 'Spanish' },
|
||||||
|
{ value: 'it', label: 'Italian' },
|
||||||
|
{ value: 'de', label: 'German' },
|
||||||
|
{ value: 'tr', label: 'Turkish' },
|
||||||
|
{ value: 'pl', label: 'Polish' },
|
||||||
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col gap-4">
|
||||||
<label for="slug-input" class="text-sm font-medium text-primary">
|
|
||||||
Add Comic Slug
|
|
||||||
<span class="text-primary/60">(e.g., "01-one-piece" from https://comick.io/comic/01-one-piece)</span>
|
|
||||||
</label>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<div class="flex-1">
|
||||||
type="text"
|
<input
|
||||||
id="slug-input"
|
type="text"
|
||||||
placeholder="Enter comic slug..."
|
id="search-input"
|
||||||
class="flex-1 px-4 py-2 rounded-md bg-secondary text-primary border border-hover focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent transition-colors"
|
placeholder="Search for comics..."
|
||||||
/>
|
class="w-full px-4 py-2 bg-primary rounded-lg border border-hover/30 focus:outline-none focus:border-accent/50 transition-colors"
|
||||||
<button
|
/>
|
||||||
id="add-slug"
|
</div>
|
||||||
class="px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
<select
|
||||||
|
id="lang-select"
|
||||||
|
class="px-4 py-2 bg-primary rounded-lg border border-hover/30 focus:outline-none focus:border-accent/50 transition-colors"
|
||||||
>
|
>
|
||||||
Add
|
{languages.map(lang => (
|
||||||
</button>
|
<option value={lang.value}>{lang.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col space-y-2">
|
<div id="search-results" class="hidden space-y-2">
|
||||||
<label for="lang-select" class="text-sm font-medium text-primary">
|
<!-- Search results will be populated here -->
|
||||||
Select Language
|
</div>
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="lang-select"
|
|
||||||
class="px-4 py-2 rounded-md bg-secondary text-primary border border-hover focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent transition-colors"
|
|
||||||
>
|
|
||||||
{availableLanguages.map(lang => (
|
|
||||||
<option value={lang}>
|
|
||||||
{languageMap[lang as keyof typeof languageMap]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="slug-list" class="space-y-2">
|
<div id="slug-list" class="space-y-2">
|
||||||
{currentSlugs.map(slug => (
|
{currentSlugs.map(slug => (
|
||||||
<div class="flex items-center justify-between px-4 py-2 bg-secondary rounded-md group" data-slug={slug}>
|
<div class="flex items-center justify-between p-3 bg-primary rounded-lg border border-hover/30" data-slug={slug}>
|
||||||
<span class="text-primary">{slug}</span>
|
<span class="text-primary">{slug}</span>
|
||||||
<button
|
<button
|
||||||
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
class="text-red-500 hover:text-red-600 transition-colors"
|
||||||
data-remove={slug}
|
data-remove={slug}
|
||||||
>
|
>
|
||||||
Remove
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="flex justify-center">
|
||||||
<a
|
<a
|
||||||
id="generate-rss"
|
id="generate-rss"
|
||||||
href="#"
|
href="/rss/custom/"
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
class="inline-flex items-center gap-2 px-6 py-3 bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))] rounded-lg hover:bg-[rgb(var(--accent-light))] transition-colors"
|
||||||
|
onclick="return !!this.href.includes(',')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-5 h-5"
|
class="w-5 h-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -86,8 +90,8 @@ import { availableLanguages, languageMap } from '../lib/api';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const slugInput = document.getElementById('slug-input') as HTMLInputElement;
|
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||||
const addButton = document.getElementById('add-slug');
|
const searchResults = document.getElementById('search-results');
|
||||||
const slugList = document.getElementById('slug-list');
|
const slugList = document.getElementById('slug-list');
|
||||||
const generateRSSLink = document.getElementById('generate-rss');
|
const generateRSSLink = document.getElementById('generate-rss');
|
||||||
const langSelect = document.getElementById('lang-select') as HTMLSelectElement;
|
const langSelect = document.getElementById('lang-select') as HTMLSelectElement;
|
||||||
|
|
@ -98,84 +102,138 @@ import { availableLanguages, languageMap } from '../lib/api';
|
||||||
function updateSlugs() {
|
function updateSlugs() {
|
||||||
localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
|
localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
|
||||||
updateRSSLink();
|
updateRSSLink();
|
||||||
|
renderSlugs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRSSLink() {
|
function renderSlugs() {
|
||||||
const slugString = [...slugs].join(',');
|
|
||||||
const lang = langSelect?.value || 'en';
|
|
||||||
|
|
||||||
// Call the API to get a short feed ID
|
|
||||||
fetch('/api/create-feed', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
slugs: [...slugs],
|
|
||||||
lang
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
generateRSSLink!.setAttribute('href', `/rss/${data.feedId}.xml`);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error generating short URL:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSlugList() {
|
|
||||||
if (!slugList) return;
|
if (!slugList) return;
|
||||||
|
|
||||||
slugList.innerHTML = '';
|
slugList.innerHTML = '';
|
||||||
[...slugs].forEach(slug => {
|
[...slugs].forEach(slug => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex items-center justify-between px-4 py-2 bg-secondary rounded-md group';
|
div.className = 'flex items-center justify-between p-3 bg-primary rounded-lg border border-hover/30';
|
||||||
div.dataset.slug = slug;
|
|
||||||
div.innerHTML = `
|
const text = document.createElement('span');
|
||||||
<span class="text-primary">${slug}</span>
|
text.textContent = slug;
|
||||||
<button
|
|
||||||
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
const removeBtn = document.createElement('button');
|
||||||
data-remove="${slug}"
|
removeBtn.className = 'text-red-500 hover:text-red-600 transition-colors';
|
||||||
>
|
removeBtn.innerHTML = `
|
||||||
Remove
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</button>
|
<path d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
slugs.delete(slug);
|
||||||
|
updateSlugs();
|
||||||
|
};
|
||||||
|
|
||||||
|
div.appendChild(text);
|
||||||
|
div.appendChild(removeBtn);
|
||||||
slugList.appendChild(div);
|
slugList.appendChild(div);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSlug(slug: string) {
|
let searchTimeout: NodeJS.Timeout;
|
||||||
const sanitizedSlug = slug.trim().toLowerCase();
|
searchInput?.addEventListener('input', () => {
|
||||||
if (sanitizedSlug && !slugs.has(sanitizedSlug)) {
|
clearTimeout(searchTimeout);
|
||||||
slugs.add(sanitizedSlug);
|
searchTimeout = setTimeout(async () => {
|
||||||
updateSlugs();
|
const query = searchInput.value.trim();
|
||||||
renderSlugList();
|
if (query.length < 2) {
|
||||||
|
searchResults!.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.comick.io/v1.0/search/?page=1&limit=5&q=${encodeURIComponent(query)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
searchResults!.innerHTML = '';
|
||||||
|
searchResults!.classList.remove('hidden');
|
||||||
|
|
||||||
|
data.forEach((comic: any) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex items-center gap-3 p-3 bg-primary rounded-lg border border-hover/30 cursor-pointer hover:border-accent/50 transition-colors';
|
||||||
|
div.onclick = () => {
|
||||||
|
slugs.add(comic.slug);
|
||||||
|
updateSlugs();
|
||||||
|
searchInput.value = '';
|
||||||
|
searchResults!.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (comic.md_covers && comic.md_covers[0]) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `https://meo.comick.pictures/${comic.md_covers[0].b2key}`;
|
||||||
|
img.className = 'w-12 h-16 object-cover rounded';
|
||||||
|
img.alt = comic.title;
|
||||||
|
div.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.innerHTML = `
|
||||||
|
<div class="font-medium">${comic.title}</div>
|
||||||
|
<div class="text-sm text-primary/60">${comic.slug}</div>
|
||||||
|
`;
|
||||||
|
div.appendChild(info);
|
||||||
|
|
||||||
|
searchResults!.appendChild(div);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to search comics:', error);
|
||||||
|
searchResults!.innerHTML = `
|
||||||
|
<div class="p-3 bg-primary rounded-lg border border-red-500/30 text-red-500">
|
||||||
|
Failed to search comics. Please try again.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateRSSLink() {
|
||||||
|
const slugString = [...slugs].join(',');
|
||||||
|
const lang = langSelect?.value || 'en';
|
||||||
|
|
||||||
|
if (slugString) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/create-feed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
slugs: [...slugs],
|
||||||
|
lang
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to create feed');
|
||||||
|
|
||||||
|
const { feedId } = await response.json();
|
||||||
|
generateRSSLink!.href = `/rss/${feedId}.xml`;
|
||||||
|
generateRSSLink!.onclick = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate feed:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generateRSSLink!.href = '#';
|
||||||
|
generateRSSLink!.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
slugInput.value = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addButton?.addEventListener('click', () => addSlug(slugInput.value));
|
// Initial render
|
||||||
|
renderSlugs();
|
||||||
slugInput?.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
addSlug(slugInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
slugList?.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
const slug = target.dataset.remove;
|
|
||||||
if (slug) {
|
|
||||||
slugs.delete(slug);
|
|
||||||
updateSlugs();
|
|
||||||
renderSlugList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
langSelect?.addEventListener('change', updateRSSLink);
|
|
||||||
|
|
||||||
// Initialize RSS link and render initial list
|
|
||||||
updateRSSLink();
|
updateRSSLink();
|
||||||
renderSlugList();
|
|
||||||
|
// Update RSS link when language changes
|
||||||
|
langSelect?.addEventListener('change', () => updateRSSLink());
|
||||||
|
|
||||||
|
// Close search results when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!searchResults?.contains(e.target as Node) && !searchInput?.contains(e.target as Node)) {
|
||||||
|
searchResults?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -11,7 +11,8 @@ const languageMap = {
|
||||||
"zh": "Chinese",
|
"zh": "Chinese",
|
||||||
"sv": "Swedish",
|
"sv": "Swedish",
|
||||||
"ar": "Arabic",
|
"ar": "Arabic",
|
||||||
"de": "German"
|
"de": "German",
|
||||||
|
"ko": "Korean"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const languages = new Set(Object.keys(languageMap));
|
const languages = new Set(Object.keys(languageMap));
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ const description = "Create a custom RSS feed for your favorite comics";
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 text-center text-primary/60">
|
<div class="mt-8 text-center text-primary/60">
|
||||||
<p>Add the comic slugs from the URLs of your favorite comics on ComicK.</p>
|
<p>Search for your favorite comics using the search box above, or add their slugs manually.</p>
|
||||||
<p> Example: For <a href="https://comick.io/comic/01-one-piece">https://comick.io/comic/01-one-piece</a>, the slug is "01-one-piece"</p>
|
<p class="mt-2">Manual example: For <a href="https://comick.io/comic/01-one-piece" class="text-accent hover:underline">https://comick.io/comic/01-one-piece</a>, the slug is "01-one-piece"</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue