feat(index): add search, fix gitea to forgejo
Some checks failed
build dist / build-dist (push) Failing after 27s
Some checks failed
build dist / build-dist (push) Failing after 27s
This commit is contained in:
parent
9b03ac7253
commit
e95115d018
11 changed files with 365 additions and 72 deletions
192
src/components/ServiceSearch.ts
Normal file
192
src/components/ServiceSearch.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
import { services } from "@/config/services";
|
||||
import {
|
||||
getServiceIconPath,
|
||||
getServiceUrl,
|
||||
searchServices,
|
||||
} from "@/utils/service";
|
||||
|
||||
export class ServiceSearch {
|
||||
private isOpen = false;
|
||||
private query = "";
|
||||
private results: typeof services = [];
|
||||
private selectedIndex = 0;
|
||||
private container: HTMLDivElement | null = null;
|
||||
private searchButton: HTMLButtonElement | null = null;
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// Create search button
|
||||
this.searchButton = document.createElement("button");
|
||||
this.searchButton.className =
|
||||
"fixed bottom-4 right-4 md:hidden bg-primary text-primary-foreground p-3 rounded-full shadow-lg z-40";
|
||||
this.searchButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.3-4.3"></path>
|
||||
</svg>
|
||||
`;
|
||||
this.searchButton.addEventListener("click", () => this.open());
|
||||
document.body.appendChild(this.searchButton);
|
||||
|
||||
// Create container
|
||||
this.container = document.createElement("div");
|
||||
this.container.className =
|
||||
"fixed inset-0 bg-black/50 flex items-start justify-center pt-[10vh] md:pt-[20vh] z-50 hidden";
|
||||
this.container.innerHTML = `
|
||||
<div class="w-full max-w-2xl mx-4">
|
||||
<div class="bg-background rounded-lg shadow-lg">
|
||||
<div class="p-4 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search services... (Press Esc to close)"
|
||||
class="w-full px-4 py-2 bg-muted rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-base"
|
||||
/>
|
||||
<button class="p-2 text-muted-foreground hover:text-foreground transition-colors" aria-label="Close search">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-[70vh] md:max-h-[60vh] overflow-y-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.container);
|
||||
|
||||
// Get elements
|
||||
const input = this.container.querySelector("input");
|
||||
const resultsContainer = this.container.querySelector(".max-h-\\[70vh\\]");
|
||||
const closeButton = this.container.querySelector("button");
|
||||
|
||||
if (!input || !resultsContainer || !closeButton) return;
|
||||
|
||||
// Add event listeners
|
||||
input.addEventListener("input", (e) => {
|
||||
this.query = (e.target as HTMLInputElement).value;
|
||||
this.updateResults();
|
||||
});
|
||||
|
||||
closeButton.addEventListener("click", () => this.close());
|
||||
|
||||
document.addEventListener("keydown", this.handleKeyDown.bind(this));
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent) {
|
||||
// Open search with Ctrl+K or Cmd+K
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
this.open();
|
||||
}
|
||||
|
||||
// Close search with Escape
|
||||
if (e.key === "Escape") {
|
||||
this.close();
|
||||
}
|
||||
|
||||
// Navigate results with arrow keys
|
||||
if (this.isOpen) {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(
|
||||
this.selectedIndex + 1,
|
||||
this.results.length - 1,
|
||||
);
|
||||
this.updateResults();
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.updateResults();
|
||||
} else if (e.key === "Enter" && this.results[this.selectedIndex]) {
|
||||
e.preventDefault();
|
||||
const service = this.results[this.selectedIndex];
|
||||
window.open(getServiceUrl(service), "_blank");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateResults() {
|
||||
if (!this.container) return;
|
||||
|
||||
const resultsContainer = this.container.querySelector(".max-h-\\[70vh\\]");
|
||||
if (!resultsContainer) return;
|
||||
|
||||
if (this.query.trim()) {
|
||||
this.results = searchServices(services, this.query);
|
||||
} else {
|
||||
this.results = [];
|
||||
}
|
||||
|
||||
if (this.results.length > 0) {
|
||||
resultsContainer.innerHTML = this.results
|
||||
.map(
|
||||
(service, index) => `
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-4 py-4 text-left hover:bg-accent hover:text-accent-foreground transition-colors text-base flex items-center gap-3 ${
|
||||
index === this.selectedIndex
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}"
|
||||
>
|
||||
<div class="flex-shrink-0 w-8 h-8">
|
||||
<img
|
||||
src="${getServiceIconPath(service)}"
|
||||
alt=""
|
||||
class="w-full h-full"
|
||||
width="32"
|
||||
height="32"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<span>${service.serviceName}</span>
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Add click handlers
|
||||
resultsContainer.querySelectorAll("button").forEach((button, index) => {
|
||||
button.addEventListener("click", () => {
|
||||
const service = this.results[index];
|
||||
window.open(getServiceUrl(service), "_blank");
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
} else if (this.query) {
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="px-4 py-4 text-muted-foreground text-base">
|
||||
No services found
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultsContainer.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
private open() {
|
||||
if (!this.container) return;
|
||||
this.isOpen = true;
|
||||
this.container.classList.remove("hidden");
|
||||
const input = this.container.querySelector("input");
|
||||
if (input) {
|
||||
input.value = "";
|
||||
input.focus();
|
||||
}
|
||||
this.query = "";
|
||||
this.results = [];
|
||||
this.selectedIndex = 0;
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
private close() {
|
||||
if (!this.container) return;
|
||||
this.isOpen = false;
|
||||
this.container.classList.add("hidden");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue