Compare commits

...

2 Commits

Author SHA1 Message Date
67e3eef364 search bar 2025-06-22 07:46:43 +02:00
7eb3a08b20 i18n init 2025-06-22 07:24:36 +02:00
14 changed files with 789 additions and 65 deletions

240
client/I18N_README.md Normal file
View File

@@ -0,0 +1,240 @@
# Internationalization (i18n) Setup
This project includes a comprehensive internationalization system built with Astro 5.x. The system supports multiple languages with URL-based routing and automatic language detection.
## Features
- ✅ URL-based language routing (Spanish: `/`, English: `/en/`)
- ✅ Spanish as default language (no URL prefix)
- ✅ Automatic language detection from URL
- ✅ Language switcher component
- ✅ Type-safe translation keys
- ✅ Fallback to default language
- ✅ Middleware for automatic redirects
- ✅ SEO-friendly URLs
## Supported Languages
Currently supported languages:
- **Spanish (es)** - Default language (no URL prefix)
- **English (en)** - Secondary language (with `/en/` prefix)
## URL Structure
- **Spanish (default)**: `/`, `/elements`, `/about`
- **English**: `/en/`, `/en/elements`, `/en/about`
## How to Use
### 1. Adding New Translations
Edit the translation dictionaries in `src/lib/i18n.ts`:
```typescript
const translations = {
es: {
'your.new.key': 'Texto en español',
// ... more translations
},
en: {
'your.new.key': 'English text',
// ... more translations
}
}
```
### 2. Using Translations in Components
```astro
---
import { t, getLanguageFromPath } from '../lib/i18n';
const currentLang = getLanguageFromPath(Astro.url.pathname);
---
<h1>{t('your.new.key', currentLang)}</h1>
```
### 3. Creating Language-Specific Pages
For English pages, create them in the `src/pages/[lang]/` directory:
```astro
---
// src/pages/[lang]/about.astro (English only)
import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from '../../lib/i18n';
const { lang } = Astro.params;
if (!lang || !isSupportedLanguage(lang)) {
return Astro.redirect('/about');
}
const currentLang = lang as SupportedLanguage;
// Redirect Spanish to root
if (currentLang === DEFAULT_LANGUAGE) {
return Astro.redirect('/about');
}
---
<h1>{t('about.title', currentLang)}</h1>
```
For Spanish pages (default), create them directly in `src/pages/`:
```astro
---
// src/pages/about.astro (Spanish default)
import { t, DEFAULT_LANGUAGE } from '../lib/i18n';
const currentLang = DEFAULT_LANGUAGE;
---
<h1>{t('about.title', currentLang)}</h1>
```
### 4. Adding the Language Switcher
The language switcher is automatically included in the main layout. To add it to other components:
```astro
---
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
---
<LanguageSwitcher />
```
### 5. Creating Localized Links
Use the `getLocalizedPath` function for navigation:
```astro
---
import { getLocalizedPath, getLanguageFromPath } from '../lib/i18n';
const currentLang = getLanguageFromPath(Astro.url.pathname);
---
<a href={getLocalizedPath('/about', currentLang)}>About</a>
```
## File Structure
```
src/
├── lib/
│ └── i18n.ts # Main i18n utilities and translations
├── components/
│ ├── LanguageSwitcher.astro # Language selection component
│ └── ... # Other components
├── pages/
│ ├── [lang]/ # English-specific pages
│ │ ├── index.astro # Redirects Spanish to root
│ │ ├── elements.astro # Redirects Spanish to root
│ │ └── ...
│ ├── index.astro # Spanish default homepage
│ ├── elements.astro # Spanish default elements page
│ └── ... # Other Spanish default pages
└── middleware.ts # Language routing middleware
```
## API Reference
### Translation Functions
#### `t(key, lang?)`
Get a translation for a given key and language.
```typescript
t('nav.home', 'es') // Returns: "Inicio"
t('nav.home', 'en') // Returns: "Home"
```
#### `getLanguageFromPath(pathname)`
Extract language from URL path.
```typescript
getLanguageFromPath('/en/elements') // Returns: "en"
getLanguageFromPath('/elements') // Returns: "es" (default)
getLanguageFromPath('/') // Returns: "es" (default)
```
#### `getLocalizedPath(path, lang)`
Generate a localized URL path.
```typescript
getLocalizedPath('/elements', 'es') // Returns: "/elements"
getLocalizedPath('/elements', 'en') // Returns: "/en/elements"
```
#### `isSupportedLanguage(lang)`
Check if a language is supported.
```typescript
isSupportedLanguage('es') // Returns: true
isSupportedLanguage('fr') // Returns: false
```
### Constants
- `SUPPORTED_LANGUAGES` - Array of supported language codes (['es', 'en'])
- `DEFAULT_LANGUAGE` - Default language code ('es')
- `TranslationKey` - TypeScript type for translation keys
## Adding New Languages
1. Add the language code to `SUPPORTED_LANGUAGES` in `src/lib/i18n.ts`
2. Add translations for the new language in the `translations` object
3. Update the language switcher component if needed
4. Create language-specific pages in `src/pages/[lang]/`
Example for French:
```typescript
export const SUPPORTED_LANGUAGES = ['es', 'en', 'fr'] as const;
const translations = {
es: { /* Spanish translations */ },
en: { /* English translations */ },
fr: { /* French translations */ }
}
```
## Best Practices
1. **Use descriptive keys**: Use namespaced keys like `nav.home`, `elements.title`
2. **Keep translations organized**: Group related translations together
3. **Test all languages**: Ensure all translations are complete
4. **Use TypeScript**: Leverage the `TranslationKey` type for type safety
5. **Handle missing translations**: The system falls back to the default language
6. **Spanish first**: Always put Spanish translations first in the dictionary
## SEO Considerations
- Spanish content is served at root URLs (better for SEO)
- English content has `/en/` prefix
- Language is properly set in the HTML `lang` attribute
- Search engines can index content in multiple languages
- Use proper hreflang tags if needed for advanced SEO
## Troubleshooting
### Common Issues
1. **Translation not found**: Check if the key exists in all language dictionaries
2. **Language not detected**: Ensure the URL follows the correct pattern
3. **Redirect loops**: Check middleware configuration
4. **Type errors**: Use the `TranslationKey` type for translation keys
### Debug Mode
Enable debug logging by adding console logs in the i18n functions:
```typescript
export function t(key: TranslationKey, lang: SupportedLanguage = DEFAULT_LANGUAGE): string {
console.log(`Translating: ${key} for language: ${lang}`);
// ... rest of function
}
```

View File

@@ -0,0 +1,49 @@
---
import { SUPPORTED_LANGUAGES, getLanguageFromPath, getLocalizedPath, type SupportedLanguage } from '../lib/i18n';
// Get current language from URL
const currentPath = Astro.url.pathname;
const currentLang = getLanguageFromPath(currentPath);
// Generate language options
const languageOptions = SUPPORTED_LANGUAGES.map(lang => ({
code: lang,
name: lang === 'es' ? 'Español' : 'English',
path: getLocalizedPath(currentPath, lang),
isCurrent: lang === currentLang
}));
---
<div class="language-switcher">
<select
id="language-select"
class="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
onchange="window.location.href = this.value"
>
{languageOptions.map((option: any) => (
<option
value={option.path}
selected={option.isCurrent}
>
{option.name}
</option>
))}
</select>
</div>
<style>
.language-switcher {
display: inline-block;
}
.language-switcher select {
background-color: white;
font-size: 0.875rem;
line-height: 1.25rem;
}
.language-switcher select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
</style>

View File

@@ -1,5 +1,9 @@
---
// Navigation component for Pole Sport website
import { t, getLanguageFromPath, getLocalizedPath } from "../lib/i18n";
// Get current language from URL
const currentLang = getLanguageFromPath(Astro.url.pathname);
---
<nav class="bg-white shadow-lg">
@@ -7,18 +11,18 @@
<div class="flex justify-between items-center py-4">
<!-- Logo/Brand -->
<div class="flex items-center">
<a href="/" class="text-xl font-bold text-gray-800 hover:text-gray-600 transition-colors">
<a href={getLocalizedPath("/", currentLang)} class="text-xl font-bold text-gray-800 hover:text-gray-600 transition-colors">
Pole Book
</a>
</div>
<!-- Navigation Links -->
<div class="hidden md:flex space-x-8">
<a href="/" class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
Home
<a href={getLocalizedPath("/", currentLang)} class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
{t('nav.home', currentLang)}
</a>
<a href="/elements" class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
Elements
<a href={getLocalizedPath("/elements", currentLang)} class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
{t('nav.elements', currentLang)}
</a>
</div>
@@ -35,11 +39,11 @@
<!-- Mobile menu -->
<div id="mobile-menu" class="md:hidden hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a href="/" class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
Home
<a href={getLocalizedPath("/", currentLang)} class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
{t('nav.home', currentLang)}
</a>
<a href="/elements" class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
Elements
<a href={getLocalizedPath("/elements", currentLang)} class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
{t('nav.elements', currentLang)}
</a>
</div>
</div>

View File

@@ -1,8 +1,7 @@
---
// Import necessary components and utilities
import MarkdownComponent from "./MardownContent.astro";
import { getStrapiMedia } from "../lib/strapi";
import { getStrapiBaseUrl } from "../config/strapi";
import { getStrapiMedia, getStrapiBaseUrl } from "../lib/strapi";
import { t, getLanguageFromPath, type SupportedLanguage } from "../lib/i18n";
import type { HTMLAttributes } from "astro/types";
// Define the props interface
@@ -13,28 +12,42 @@ export interface Props extends HTMLAttributes<'div'> {
// Destructure props and get BASE_URL from global config
const { elements, ...otherProps } = Astro.props;
const BASE_URL = getStrapiBaseUrl();
// Get current language
const currentLang = getLanguageFromPath(Astro.url.pathname);
---
<div {...otherProps}>
<div {...otherProps} data-elements-container>
{
elements.map((poleElement) => (
<a href={`/elements/${poleElement.id}`} class="block">
<article class="flex items-center bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-200">
<img
src={getStrapiMedia(
poleElement.mainImage.url,
BASE_URL,
)}
alt={poleElement.mainImage.alternativeText}
class="w-24 h-24 object-cover flex-shrink-0"
/>
<div class="p-4">
<h2 class="text-xl font-bold">
{poleElement.name}
</h2>
</div>
</article>
</a>
))
elements.length === 0 ? (
<div class="text-center py-8">
<p class="text-gray-500 text-lg">{t('elements.noElements', currentLang)}</p>
</div>
) : (
<>
{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">
<img
src={getStrapiMedia(
poleElement.mainImage.url,
BASE_URL,
)}
alt={poleElement.mainImage.alternativeText}
class="w-24 h-24 object-cover flex-shrink-0"
/>
<div class="p-4">
<h2 class="text-xl font-bold">
{poleElement.name}
</h2>
</div>
</article>
</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>

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

@@ -2,16 +2,23 @@
import "../styles/global.css";
import Head from "./Head.astro";
import Navigation from "../components/Navigation.astro";
import { getLanguageFromPath } from "../lib/i18n";
const { title = "Just a title", description = "Adescription" } = Astro.props;
// Get current language from URL
const currentLang = getLanguageFromPath(Astro.url.pathname);
---
<!doctype html>
<html lang="en">
<html lang={currentLang}>
<Head {title} {description} />
<body>
<Navigation />
<div class="container max-w-4xl mx-auto p-4">
<header class="bg-white shadow-sm">
<Navigation />
</header>
<main class="container max-w-4xl mx-auto p-4">
<slot />
</div>
</main>
</body>
</html>

153
client/src/lib/i18n.ts Normal file
View File

@@ -0,0 +1,153 @@
// Supported languages
export const SUPPORTED_LANGUAGES = ['es', 'en'] as const;
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
// Default language
export const DEFAULT_LANGUAGE: SupportedLanguage = 'es';
// Translation dictionaries
const translations = {
es: {
// Common
'common.loading': 'Cargando...',
'common.error': 'Error',
'common.success': 'Éxito',
'common.cancel': 'Cancelar',
'common.save': 'Guardar',
'common.edit': 'Editar',
'common.delete': 'Eliminar',
'common.back': 'Atrás',
'common.next': 'Siguiente',
'common.previous': 'Anterior',
// Navigation
'nav.home': 'Inicio',
'nav.about': 'Acerca de',
'nav.contact': 'Contacto',
'nav.elements': 'Elementos',
// Elements
'elements.title': 'Elementos',
'elements.view': 'Ver Elemento',
'elements.noElements': 'No se encontraron 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
'form.required': 'Este campo es obligatorio',
'form.invalid': 'Entrada inválida',
'form.submit': 'Enviar',
'form.reset': 'Restablecer',
// Messages
'message.elementNotFound': 'Elemento no encontrado',
'message.loadingError': 'Error al cargar contenido',
'message.saveSuccess': 'Guardado exitosamente',
'message.deleteSuccess': 'Eliminado exitosamente',
},
en: {
// Common
'common.loading': 'Loading...',
'common.error': 'Error',
'common.success': 'Success',
'common.cancel': 'Cancel',
'common.save': 'Save',
'common.edit': 'Edit',
'common.delete': 'Delete',
'common.back': 'Back',
'common.next': 'Next',
'common.previous': 'Previous',
// Navigation
'nav.home': 'Home',
'nav.about': 'About',
'nav.contact': 'Contact',
'nav.elements': 'Elements',
// Elements
'elements.title': 'Elements',
'elements.view': 'View Element',
'elements.noElements': 'No elements found',
'elements.loading': 'Loading elements...',
'elements.search': 'Search elements...',
'elements.searchPlaceholder': 'Search by name...',
'elements.noResults': 'No elements found matching your search',
// Forms
'form.required': 'This field is required',
'form.invalid': 'Invalid input',
'form.submit': 'Submit',
'form.reset': 'Reset',
// Messages
'message.elementNotFound': 'Element not found',
'message.loadingError': 'Error loading content',
'message.saveSuccess': 'Successfully saved',
'message.deleteSuccess': 'Successfully deleted',
}
} as const;
// Type for translation keys
export type TranslationKey = keyof typeof translations.es;
/**
* Get translation for a given key and language
*/
export function t(key: TranslationKey, lang: SupportedLanguage = DEFAULT_LANGUAGE): string {
const langTranslations = translations[lang];
if (!langTranslations) {
console.warn(`Language ${lang} not supported, falling back to ${DEFAULT_LANGUAGE}`);
return translations[DEFAULT_LANGUAGE][key] || key;
}
return langTranslations[key] || translations[DEFAULT_LANGUAGE][key] || key;
}
/**
* Get current language from URL path
*/
export function getLanguageFromPath(pathname: string): SupportedLanguage {
const pathSegments = pathname.split('/').filter(Boolean);
const firstSegment = pathSegments[0];
if (SUPPORTED_LANGUAGES.includes(firstSegment as SupportedLanguage)) {
return firstSegment as SupportedLanguage;
}
return DEFAULT_LANGUAGE;
}
/**
* Get path with language prefix
*/
export function getLocalizedPath(path: string, lang: SupportedLanguage): string {
if (lang === DEFAULT_LANGUAGE) {
return path;
}
// Remove leading slash if present
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
return `/${lang}/${cleanPath}`;
}
/**
* Get all translations for a given key across all languages
*/
export function getAllTranslations(key: TranslationKey): Record<SupportedLanguage, string> {
const result: Record<SupportedLanguage, string> = {} as Record<SupportedLanguage, string>;
SUPPORTED_LANGUAGES.forEach(lang => {
result[lang] = t(key, lang);
});
return result;
}
/**
* Check if a language is supported
*/
export function isSupportedLanguage(lang: string): lang is SupportedLanguage {
return SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage);
}

26
client/src/middleware.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineMiddleware } from 'astro:middleware';
import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, isSupportedLanguage } from './lib/i18n';
export const onRequest = defineMiddleware(async (context, next) => {
const { pathname } = context.url;
// Skip middleware for static assets
if (pathname.startsWith('/_astro/') || pathname.startsWith('/assets/') || pathname.includes('.')) {
return next();
}
const pathSegments = pathname.split('/').filter(Boolean);
const firstSegment = pathSegments[0];
// If the first segment is a supported language, continue
if (isSupportedLanguage(firstSegment)) {
return next();
}
// If no language prefix, it's Spanish (default) - continue
if (pathname === '/' || !isSupportedLanguage(firstSegment)) {
return next();
}
return next();
});

View File

@@ -0,0 +1,53 @@
---
// Import necessary components and utilities
import Layout from "../../layouts/Layout.astro";
import PoleElementsList from "../../components/PoleElementsList.astro";
import SearchBar from "../../components/SearchBar.astro";
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
const { lang } = Astro.params;
// Validate language parameter
if (!lang || !isSupportedLanguage(lang)) {
return Astro.redirect('/elements');
}
const currentLang = lang as SupportedLanguage;
// If it's the default language (Spanish), redirect to root
if (currentLang === DEFAULT_LANGUAGE) {
return Astro.redirect('/elements');
}
// 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 elements = await fetchApi<PoleElement[]>({
endpoint: 'elements',
query: queryParams,
wrappedByKey: 'data',
});
---
<Layout title={t('elements.title', currentLang)} description="Pole Elements">
<div class="container mx-auto p-4">
<div class="py-8">
<h1 class="text-3xl font-bold mb-8">{t('elements.title', currentLang)}</h1>
<SearchBar />
<PoleElementsList elements={elements} />
</div>
</div>
</Layout>

View File

@@ -0,0 +1,34 @@
---
// Import necessary components and utilities
import Layout from "../../layouts/Layout.astro";
import BigCard from "../../components/BigCard.astro";
import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from "../../lib/i18n";
// Get language from URL parameter
const { lang } = Astro.params;
// Validate language parameter
if (!lang || !isSupportedLanguage(lang)) {
return Astro.redirect('/');
}
const currentLang = lang as SupportedLanguage;
// If it's the default language (Spanish), redirect to root
if (currentLang === DEFAULT_LANGUAGE) {
return Astro.redirect('/');
}
---
<Layout title={t('nav.home', currentLang)} description="Pole Elements">
<div class="container mx-auto p-4">
<div class="py-12">
<BigCard
url={`/${currentLang}/elements`}
title={t('elements.title', currentLang)}
description="Explore a comprehensive collection of pole dance elements, techniques, and combinations. From basic spins to advanced tricks, discover everything you need to master pole sport."
ctaText={t('elements.view', currentLang)}
/>
</div>
</div>
</Layout>

View File

@@ -2,26 +2,48 @@
// Import necessary components and utilities
import Layout from "../layouts/Layout.astro";
import PoleElementsList from "../components/PoleElementsList.astro";
import SearchBar from "../components/SearchBar.astro";
import { t, DEFAULT_LANGUAGE } from "../lib/i18n";
import fetchApi from '../lib/strapi';
import fetchApi from "../lib/strapi";
import type PoleElement from "../interfaces/poleElement";
// Use Spanish as 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[]>({
endpoint: 'elements?populate=*', // the content type to fetch
wrappedByKey: 'data', // the key to unwrap the response
endpoint: "elements",
query: queryParams,
wrappedByKey: "data",
});
---
<Layout title="Pole Elements" description="Pole Elements">
<!-- Main heading -->
<h1 class="text-3xl font-bold mb-8">
Pole Elements
</h1>
<!-- Pole Elements List -->
<PoleElementsList
elements={strapiPoleElements}
class="max-w-4xl mx-auto space-y-4"
id="pole-elements-list"
data-testid="pole-elements"
/>
<Layout title={t("elements.title", currentLang)} description="Pole Elements">
<div class="container mx-auto p-4">
<div class="py-8">
<h1 class="text-3xl font-bold mb-8">
{t("elements.title", currentLang)}
</h1>
<SearchBar />
<PoleElementsList
elements={strapiPoleElements}
class="max-w-4xl mx-auto space-y-4"
id="pole-elements-list"
data-testid="pole-elements"
/>
</div>
</div>
</Layout>

View File

@@ -2,16 +2,20 @@
// Import necessary components and utilities
import Layout from "../layouts/Layout.astro";
import BigCard from "../components/BigCard.astro";
import { t, DEFAULT_LANGUAGE } from "../lib/i18n";
// Use Spanish as default language
const currentLang = DEFAULT_LANGUAGE;
---
<Layout title="Pole Elements" description="Pole Elements">
<Layout title={t('nav.home', currentLang)} description="Pole Elements">
<div class="container mx-auto p-4">
<div class="py-12">
<BigCard
url="/elements"
title="Pole Elements"
title={t('elements.title', currentLang)}
description="Explore a comprehensive collection of pole dance elements, techniques, and combinations. From basic spins to advanced tricks, discover everything you need to master pole sport."
ctaText="Explore Elements"
ctaText={t('elements.view', currentLang)}
/>
</div>
</div>

View File

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