search bar

This commit is contained in:
2025-06-22 07:46:43 +02:00
parent 7eb3a08b20
commit 67e3eef364
7 changed files with 202 additions and 55 deletions

View File

@@ -1,8 +1,6 @@
--- ---
// Import necessary components and utilities // Import necessary components and utilities
import MarkdownComponent from "./MardownContent.astro"; import { getStrapiMedia, getStrapiBaseUrl } from "../lib/strapi";
import { getStrapiMedia } from "../lib/strapi";
import { getStrapiBaseUrl } from "../config/strapi";
import { t, getLanguageFromPath, type SupportedLanguage } from "../lib/i18n"; import { t, getLanguageFromPath, type SupportedLanguage } from "../lib/i18n";
import type { HTMLAttributes } from "astro/types"; import type { HTMLAttributes } from "astro/types";
@@ -19,15 +17,16 @@ const BASE_URL = getStrapiBaseUrl();
const currentLang = getLanguageFromPath(Astro.url.pathname); const currentLang = getLanguageFromPath(Astro.url.pathname);
--- ---
<div {...otherProps}> <div {...otherProps} data-elements-container>
{ {
elements.length === 0 ? ( elements.length === 0 ? (
<div class="text-center py-8"> <div class="text-center py-8">
<p class="text-gray-500 text-lg">{t('elements.noElements', currentLang)}</p> <p class="text-gray-500 text-lg">{t('elements.noElements', currentLang)}</p>
</div> </div>
) : ( ) : (
elements.map((poleElement) => ( <>
<a href={`/elements/${poleElement.id}`} class="block"> {elements.map((poleElement) => (
<a href={`/elements/${poleElement.id}`} class="block mb-4">
<article class="flex items-center bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-200"> <article class="flex items-center bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-200">
<img <img
src={getStrapiMedia( src={getStrapiMedia(
@@ -44,7 +43,11 @@ const currentLang = getLanguageFromPath(Astro.url.pathname);
</div> </div>
</article> </article>
</a> </a>
)) ))}
<div id="no-results-message" class="text-center py-8 hidden">
<p class="text-gray-500 text-lg">{t('elements.noResults', currentLang)}</p>
</div>
</>
) )
} }
</div> </div>

View File

@@ -0,0 +1,129 @@
---
// Import necessary utilities
import { t, getLanguageFromPath, type SupportedLanguage } from "../lib/i18n";
// Define the props interface
export interface Props {
placeholder?: string;
onSearch?: (query: string) => void;
}
// Destructure props
const { placeholder, onSearch } = Astro.props;
// Get current language
const currentLang = getLanguageFromPath(Astro.url.pathname);
const defaultPlaceholder = t('elements.searchPlaceholder', currentLang);
// Get current search query from URL
const currentSearchQuery = Astro.url.searchParams.get('search') || '';
---
<div class="mb-6">
<div class="relative">
<input
type="text"
id="search-input"
placeholder={placeholder || defaultPlaceholder}
value={currentSearchQuery}
class="w-full px-4 py-3 pl-12 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
/>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<button
id="clear-search"
class="absolute inset-y-0 right-0 pr-3 flex items-center opacity-0 pointer-events-none transition-opacity duration-200 hover:text-gray-600"
aria-label="Clear search"
>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<script>
// Client-side search functionality with API calls
const searchInput = document.getElementById('search-input') as HTMLInputElement;
const clearButton = document.getElementById('clear-search') as HTMLButtonElement;
const elementsContainer = document.querySelector('[data-elements-container]');
if (searchInput && elementsContainer) {
let searchTimeout: NodeJS.Timeout;
// Function to update clear button visibility
const updateClearButton = () => {
const hasValue = searchInput.value.length > 0;
clearButton.style.opacity = hasValue ? '1' : '0';
clearButton.style.pointerEvents = hasValue ? 'auto' : 'none';
};
// Function to fetch filtered data from API
const fetchFilteredElements = async (query: string) => {
try {
// Show loading state
if (elementsContainer) {
elementsContainer.innerHTML = '<div class="text-center py-8"><p class="text-gray-500 text-lg">Searching...</p></div>';
}
// Build the API URL with search parameters
const currentUrl = new URL(window.location.href);
if (query.trim()) {
currentUrl.searchParams.set('search', query.trim());
} else {
currentUrl.searchParams.delete('search');
}
// Fetch the new page content
const response = await fetch(currentUrl.toString());
const html = await response.text();
// Parse the HTML and extract the elements container
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newElementsContainer = doc.querySelector('[data-elements-container]');
if (newElementsContainer && elementsContainer) {
elementsContainer.innerHTML = newElementsContainer.innerHTML;
}
// Update URL without page reload
window.history.pushState({}, '', currentUrl.toString());
} catch (error) {
console.error('Error fetching filtered elements:', error);
if (elementsContainer) {
elementsContainer.innerHTML = '<div class="text-center py-8"><p class="text-red-500 text-lg">Error loading results</p></div>';
}
}
};
// Search input event listener with debouncing
searchInput.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value;
updateClearButton();
// Clear previous timeout
clearTimeout(searchTimeout);
// Set new timeout for debounced search
searchTimeout = setTimeout(() => {
fetchFilteredElements(query);
}, 300); // 300ms delay
});
// Clear button event listener
clearButton.addEventListener('click', () => {
searchInput.value = '';
updateClearButton();
fetchFilteredElements('');
searchInput.focus();
});
// Initialize clear button state
updateClearButton();
}
</script>

View File

@@ -1,9 +0,0 @@
// Global Strapi configuration
export const STRAPI_CONFIG = {
BASE_URL: (import.meta.env.STRAPI_URL as string) || (process.env.STRAPI_URL as string) || "http://localhost:1337",
} as const;
// Helper function to get the base URL
export function getStrapiBaseUrl(): string {
return STRAPI_CONFIG.BASE_URL;
}

View File

@@ -31,6 +31,9 @@ const translations = {
'elements.view': 'Ver Elemento', 'elements.view': 'Ver Elemento',
'elements.noElements': 'No se encontraron elementos', 'elements.noElements': 'No se encontraron elementos',
'elements.loading': 'Cargando elementos...', 'elements.loading': 'Cargando elementos...',
'elements.search': 'Buscar elementos...',
'elements.searchPlaceholder': 'Buscar por nombre...',
'elements.noResults': 'No se encontraron elementos que coincidan con tu búsqueda',
// Forms // Forms
'form.required': 'Este campo es obligatorio', 'form.required': 'Este campo es obligatorio',
@@ -68,6 +71,9 @@ const translations = {
'elements.view': 'View Element', 'elements.view': 'View Element',
'elements.noElements': 'No elements found', 'elements.noElements': 'No elements found',
'elements.loading': 'Loading elements...', 'elements.loading': 'Loading elements...',
'elements.search': 'Search elements...',
'elements.searchPlaceholder': 'Search by name...',
'elements.noResults': 'No elements found matching your search',
// Forms // Forms
'form.required': 'This field is required', 'form.required': 'This field is required',

View File

@@ -2,7 +2,10 @@
// Import necessary components and utilities // Import necessary components and utilities
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import PoleElementsList from "../../components/PoleElementsList.astro"; import PoleElementsList from "../../components/PoleElementsList.astro";
import SearchBar from "../../components/SearchBar.astro";
import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from "../../lib/i18n"; import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from "../../lib/i18n";
import fetchApi from "../../lib/strapi";
import type PoleElement from "../../interfaces/poleElement";
// Get language from URL parameter // Get language from URL parameter
const { lang } = Astro.params; const { lang } = Astro.params;
@@ -19,31 +22,31 @@ if (currentLang === DEFAULT_LANGUAGE) {
return Astro.redirect('/elements'); return Astro.redirect('/elements');
} }
// Mock data for demonstration - replace with actual data fetching // Get search query from URL parameters
const elements = [ const searchQuery = Astro.url.searchParams.get('search') || '';
{
id: 1, // Build query parameters for API
name: "Basic Spin", const queryParams: Record<string, string> = {
mainImage: { 'populate': '*'
url: "/placeholder-image.jpg", };
alternativeText: "Basic Spin"
} // Add search filter if query exists
}, if (searchQuery.trim()) {
{ queryParams['filters[name][$containsi]'] = searchQuery.trim();
id: 2, }
name: "Fireman Spin",
mainImage: { const elements = await fetchApi<PoleElement[]>({
url: "/placeholder-image.jpg", endpoint: 'elements',
alternativeText: "Fireman Spin" query: queryParams,
} wrappedByKey: 'data',
} });
];
--- ---
<Layout title={t('elements.title', currentLang)} description="Pole Elements"> <Layout title={t('elements.title', currentLang)} description="Pole Elements">
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<div class="py-8"> <div class="py-8">
<h1 class="text-3xl font-bold mb-8">{t('elements.title', currentLang)}</h1> <h1 class="text-3xl font-bold mb-8">{t('elements.title', currentLang)}</h1>
<SearchBar />
<PoleElementsList elements={elements} /> <PoleElementsList elements={elements} />
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
// Import necessary components and utilities // Import necessary components and utilities
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
import PoleElementsList from "../components/PoleElementsList.astro"; import PoleElementsList from "../components/PoleElementsList.astro";
import SearchBar from "../components/SearchBar.astro";
import { t, DEFAULT_LANGUAGE } from "../lib/i18n"; import { t, DEFAULT_LANGUAGE } from "../lib/i18n";
import fetchApi from "../lib/strapi"; import fetchApi from "../lib/strapi";
@@ -10,9 +11,23 @@ import type PoleElement from "../interfaces/poleElement";
// Use Spanish as default language // Use Spanish as default language
const currentLang = DEFAULT_LANGUAGE; const currentLang = DEFAULT_LANGUAGE;
// Get search query from URL parameters
const searchQuery = Astro.url.searchParams.get('search') || '';
// Build query parameters for API
const queryParams: Record<string, string> = {
'populate': '*'
};
// Add search filter if query exists
if (searchQuery.trim()) {
queryParams['filters[name][$containsi]'] = searchQuery.trim();
}
const strapiPoleElements = await fetchApi<PoleElement[]>({ const strapiPoleElements = await fetchApi<PoleElement[]>({
endpoint: "elements?populate=*", // the content type to fetch endpoint: "elements",
wrappedByKey: "data", // the key to unwrap the response query: queryParams,
wrappedByKey: "data",
}); });
--- ---
@@ -22,6 +37,7 @@ const strapiPoleElements = await fetchApi<PoleElement[]>({
<h1 class="text-3xl font-bold mb-8"> <h1 class="text-3xl font-bold mb-8">
{t("elements.title", currentLang)} {t("elements.title", currentLang)}
</h1> </h1>
<SearchBar />
<PoleElementsList <PoleElementsList
elements={strapiPoleElements} elements={strapiPoleElements}
class="max-w-4xl mx-auto space-y-4" class="max-w-4xl mx-auto space-y-4"

View File

@@ -1,7 +1,6 @@
--- ---
import MardownContent from '../components/MardownContent.astro'; import MardownContent from '../components/MardownContent.astro';
import { getStrapiMedia } from '../lib/strapi'; import { getStrapiMedia, getStrapiBaseUrl } from '../lib/strapi';
import { getStrapiBaseUrl } from '../config/strapi';
interface Props { interface Props {
entry: { entry: {