Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
integrations: [react(), tailwind()],
output: "hybrid",
output: "server", //El output server es necesario para que i18n funcione, para mas info mirar la documentacion del mismo
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains spelling errors. It should be 'más' (with accent) and 'documentación' (with accent).

Suggested change
output: "server", //El output server es necesario para que i18n funcione, para mas info mirar la documentacion del mismo
output: "server", //El output server es necesario para que i18n funcione, para más info mirar la documentación del mismo

Copilot uses AI. Check for mistakes.
adapter: cloudflare({
}),
image: {
domains: ["yellowumbrella.dev"],
},
i18n: {
locales: ["es", "en"],
defaultLocale: "es",
routing: {
prefixDefaultLocale: true
}
}
});
56 changes: 32 additions & 24 deletions src/components/ContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PhoneInput, { isValidPhoneNumber } from "react-phone-number-input";
import { Turnstile } from "@marsidev/react-turnstile";
import type { TurnstileInstance } from "@marsidev/react-turnstile";
import "react-phone-number-input/style.css";
import { useTranslationsReact, getLangFromUrlReact } from '../i18n/react-utils';

type FormInputs = {
empresa: string;
Expand All @@ -13,8 +14,15 @@ type FormInputs = {
mensaje: string;
};

interface ContactFormProps {
lang?: string;
}

export default function ContactForm() {
export default function ContactForm({ lang }: ContactFormProps = {}) {
// Detectar idioma desde la URL del navegador si no se proporciona
const currentLang = lang || getLangFromUrlReact(window.location.pathname);
const t = useTranslationsReact(currentLang as 'es' | 'en');

Comment on lines +23 to +25
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing window.location.pathname during server-side rendering will cause an error since window is undefined on the server. This should be handled with proper client-side detection or the lang prop should be required.

Suggested change
const currentLang = lang || getLangFromUrlReact(window.location.pathname);
const t = useTranslationsReact(currentLang as 'es' | 'en');
const [currentLang, setCurrentLang] = React.useState<string>(lang || 'es');
React.useEffect(() => {
if (!lang && typeof window !== 'undefined') {
setCurrentLang(getLangFromUrlReact(window.location.pathname));
}
}, [lang]);
const t = useTranslationsReact(currentLang as 'es' | 'en');

Copilot uses AI. Check for mistakes.
const {
register,
handleSubmit,
Expand Down Expand Up @@ -42,7 +50,7 @@ export default function ContactForm() {
}

if (!token) {
setStatus({ type: "error", message: "Por favor, completa el captcha antes de enviar" });
setStatus({ type: "error", message: t('contact.form.captcha.required') });
return;
}

Expand All @@ -65,7 +73,7 @@ export default function ContactForm() {

if (!res.ok) {
const text = await res.text();
throw new Error(text || "Error al enviar el formulario");
throw new Error(text || t('contact.form.error'));
}

setStatus({ type: "success" });
Expand All @@ -75,7 +83,7 @@ export default function ContactForm() {
// Reset Turnstile widget using ref
turnstileRef.current?.reset();
} catch (err: unknown) {
setStatus({ type: "error", message: (err instanceof Error ? err.message : String(err)) || "Error inesperado" });
setStatus({ type: "error", message: (err instanceof Error ? err.message : String(err)) || t('contact.form.error') });
}
};

Expand All @@ -89,72 +97,72 @@ export default function ContactForm() {
>
<div>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="empresa">
Nombre de la empresa
{t('contact.form.company')}
</label>
<input
id="empresa"
className="w-full rounded-md border border-white/20 bg-white/10 p-3 text-white placeholder-white/50 focus:border-yellow-400 focus:outline-none"
placeholder="Yellow Umbrella Tech"
placeholder={t('contact.form.company.placeholder')}
{...register("empresa", { required: true })}
/>
{errors.empresa && (
<p className="mt-1 text-sm text-red-400">Este campo es obligatorio</p>
<p className="mt-1 text-sm text-red-400">{t('contact.form.company.required')}</p>
)}
</div>

<div>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="nombreCompleto">
Nombre y apellidos
{t('contact.form.name')}
</label>
<input
id="nombreCompleto"
className="w-full rounded-md border border-white/20 bg-white/10 p-3 text-white placeholder-white/50 focus:border-yellow-400 focus:outline-none"
placeholder="Ana García Rodríguez"
placeholder={t('contact.form.name.placeholder')}
{...register("nombreCompleto", { required: true, minLength: 2 })}
/>
{errors.nombreCompleto && (
<p className="mt-1 text-sm text-red-400">Introduce tu nombre completo</p>
<p className="mt-1 text-sm text-red-400">{t('contact.form.name.required')}</p>
)}
</div>

<div>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="cargo">
Cargo en la empresa (opcional)
{t('contact.form.position')}
</label>
<input
id="cargo"
className="w-full rounded-md border border-white/20 bg-white/10 p-3 text-white placeholder-white/50 focus:border-yellow-400 focus:outline-none"
placeholder="CEO, CTO, etc."
placeholder={t('contact.form.position.placeholder')}
{...register("cargo")}
/>
</div>

<div>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="email">
Email
{t('contact.form.email')}
</label>
<input
id="email"
type="email"
className="w-full rounded-md border border-white/20 bg-white/10 p-3 text-white placeholder-white/50 focus:border-yellow-400 focus:outline-none"
placeholder="ana.garcia@empresa.com"
placeholder={t('contact.form.email.placeholder')}
{...register("email", {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
})}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-400">Introduce un email válido</p>
<p className="mt-1 text-sm text-red-400">{t('contact.form.email.required')}</p>
)}
</div>

<div>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="telefono">Teléfono (opcional)</label>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="telefono">{t('contact.form.phone')}</label>
<div className="rounded-md border border-white/20 bg-white/10 p-2 text-white focus-within:border-yellow-400">
<PhoneInput
id="telefono"
defaultCountry="ES"
placeholder="+34 600 123 456"
defaultCountry={currentLang === 'es' ? "ES" : "US"}
placeholder={t('contact.form.phone.placeholder')}
value={telefono}
onChange={setTelefono}
smartCaret={false}
Expand All @@ -164,20 +172,20 @@ export default function ContactForm() {
</div>
{telefono && typeof telefono === "string" && !isValidPhoneNumber(telefono) && (
<p className="mt-1 text-sm text-yellow-300">
El número puede no ser válido. Revisa el país y formato.
{t('contact.form.phone.invalid')}
</p>
)}
</div>

<div>
<label className="mb-1 block text-sm font-medium text-white" htmlFor="mensaje">
Mensaje
{t('contact.form.message')}
</label>
<textarea
id="mensaje"
rows={5}
className="w-full rounded-md border border-white/20 bg-white/10 p-3 text-white placeholder-white/50 focus:border-yellow-400 focus:outline-none"
placeholder="Cuéntanos en qué podemos ayudarte..."
placeholder={t('contact.form.message.placeholder')}
{...register("mensaje", {})}
/>
</div>
Expand All @@ -188,7 +196,7 @@ export default function ContactForm() {
siteKey={siteKey}
options={{
theme: "dark",
language: "es",
language: currentLang as "es" | "en",
}}
onSuccess={(token) => {
setTurnstileToken(token);
Expand All @@ -212,13 +220,13 @@ export default function ContactForm() {
className="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]"
>
<span className="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
{isSubmitting ? "Enviando..." : "Enviar"}
{isSubmitting ? t('contact.form.sending') : t('contact.form.send')}
</span>
</button>
</div>

{status.type === "success" && (
<p className="text-sm text-green-400">¡Gracias! Hemos recibido tu solicitud.</p>
<p className="text-sm text-green-400">{t('contact.form.success')}</p>
)}
{status.type === "error" && (
<p className="text-sm text-red-400">{status.message}</p>
Expand Down
25 changes: 15 additions & 10 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
@@ -1,42 +1,47 @@
---
import Umbrella from '../components/svg/Umbrella.astro'
import { getLangFromUrl, useTranslations } from '../i18n/utils';
import { getRelativeLocaleUrl } from 'astro:i18n';

const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
---

<div class="place-items-center mt-10 m-4">
<div class="flex place-content-center">
<a href="/">
<a href={getRelativeLocaleUrl(lang, '')}>
<Umbrella/>
</a>
</div>
<p class="text-2xl text-white text-center">It's raining outside, take this</p>
<h1 class="text-6xl text-[#ffd300] font-bold text-center">Yellow Umbrella</h1>
<p class="text-2xl text-white text-center">{t('home.subtitle')}</p>
<h1 class="text-6xl text-[#ffd300] font-bold text-center">{t('home.title')}</h1>
<nav class="mt-5">
<ul class="flex flex-wrap justify-center gap-2">
<a href="/sobre-nosotros">
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'sobre-nosotros' : 'about-us')}>
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
¿Quiénes somos?
{t('home.nav.about')}
</span>
</button>
</a>
<a href="https://blog.yellowumbrella.dev" target="_blank" rel="noreferrer">
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
Blog
{t('home.nav.blog')}
</span>
</button>
</a>
<a href="/redes-sociales">
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'redes-sociales' : 'social-networks')}>
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
Redes sociales
{t('home.nav.social')}
</span>
</button>
</a>
<a href="/contacto">
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'contacto' : 'contact')}>
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
Contacto
{t('home.nav.contact')}
</span>
</button>
</a>
Comment on lines +20 to 47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid nested interactive elements (button inside anchor)

An anchor containing a button is invalid and harms accessibility. Style the anchor as a button instead.

Apply this diff:

-            <a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'sobre-nosotros' : 'about-us')}>
-                <button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2  mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
-                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
+            <a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'sobre-nosotros' : 'about-us')}
+               class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
+                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
                         {t('home.nav.about')}
                     </span>
-                </button>
             </a>
-            <a href="https://blog.yellowumbrella.dev" target="_blank" rel="noreferrer">
-                <button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
-                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
+            <a href="https://blog.yellowumbrella.dev" target="_blank" rel="noopener noreferrer"
+               class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
+                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
                         {t('home.nav.blog')}
                     </span>
-                </button>
             </a>
-            <a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'redes-sociales' : 'social-networks')}>
-                <button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
-                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
+            <a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'redes-sociales' : 'social-networks')}
+               class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
+                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
                         {t('home.nav.social')}
                     </span>
-                </button>
             </a>
-            <a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'contacto' : 'contact')}>
-                <button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
-                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
+            <a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'contacto' : 'contact')}
+               class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
+                    <span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
                         {t('home.nav.contact')}
                     </span>
-                </button>
             </a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'sobre-nosotros' : 'about-us')}>
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
¿Quiénes somos?
{t('home.nav.about')}
</span>
</button>
</a>
<a href="https://blog.yellowumbrella.dev" target="_blank" rel="noreferrer">
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
Blog
{t('home.nav.blog')}
</span>
</button>
</a>
<a href="/redes-sociales">
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'redes-sociales' : 'social-networks')}>
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
Redes sociales
{t('home.nav.social')}
</span>
</button>
</a>
<a href="/contacto">
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'contacto' : 'contact')}>
<button class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
Contacto
{t('home.nav.contact')}
</span>
</button>
</a>
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'sobre-nosotros' : 'about-us')}
class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
{t('home.nav.about')}
</span>
</a>
<a href="https://blog.yellowumbrella.dev" target="_blank" rel="noopener noreferrer"
class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
{t('home.nav.blog')}
</span>
</a>
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'redes-sociales' : 'social-networks')}
class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-lg lg:text-base md:text-sm sm:text-xs text-center leading-tight">
{t('home.nav.social')}
</span>
</a>
<a href={getRelativeLocaleUrl(lang, lang === 'es' ? 'contacto' : 'contact')}
class="relative inline-flex items-center justify-center w-32 h-16 p-[2px] mb-2 mr-4 overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-[#ffd300] to-[#773376] group-hover:from-[#ffd300] group-hover:to-[#773376] hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-[#773376] dark:focus:ring-[#773376]">
<span class="relative w-full h-full px-2 py-1 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-[6px] group-hover:bg-opacity-0 flex items-center justify-center text-xl lg:text-lg md:text-base sm:text-sm text-center leading-tight">
{t('home.nav.contact')}
</span>
</a>
🤖 Prompt for AI Agents
In src/components/Head.astro lines 20-47: the markup nests <button> inside <a>,
which is invalid and inaccessible; remove the inner <button> elements and move
their classes/attributes to the parent <a> so each anchor is styled like a
button (keep href, target and rel for external links), preserve the inner <span>
content and classes, and ensure the anchor retains the focus/hover styles (add
any focus-related attributes to the <a> as needed) so anchors remain
keyboard-focusable and visually identical to the previous button styling.

Expand Down
57 changes: 57 additions & 0 deletions src/components/LanguagePicker.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
import { languages } from '../i18n/ui';
import { getLangFromUrl, useTranslatedPath, getRouteFromUrl } from '../i18n/utils';
import { getRelativeLocaleUrl } from 'astro:i18n';

const lang = getLangFromUrl(Astro.url);
const translatePath = useTranslatedPath(lang);

// Obtener la ruta actual sin el prefijo del idioma
const currentRoute = getRouteFromUrl(Astro.url) || '';
---

<div class="fixed top-5 right-5 z-[1000]">
<select
id="language-select"
class="bg-gray-900 text-white border border-gray-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#ffd300] focus:border-transparent"
>
{Object.entries(languages).map(([langCode, label]) => {
const translatedRoute = translatePath(`/${currentRoute}`, langCode);
return (
<option
value={langCode}
selected={langCode === lang}
data-url={translatedRoute}
>
{label}
</option>
);
})}
</select>
</div>

<script>
function initLanguagePicker() {
const select = document.getElementById('language-select') as HTMLSelectElement;

if (select) {
// Remover event listeners previos para evitar duplicados
const newSelect = select.cloneNode(true) as HTMLSelectElement;
select.parentNode?.replaceChild(newSelect, select);

newSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
const url = selectedOption.getAttribute('data-url');
if (url) {
window.location.href = url;
}
});
}
}

// Inicializar cuando se carga la página
document.addEventListener('DOMContentLoaded', initLanguagePicker);

// Reinicializar después de cada transición de página de Astro
document.addEventListener('astro:page-load', initLanguagePicker);
</script>
Comment on lines +33 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove TypeScript from browser script to avoid runtime errors

The script tag runs in the browser; TS assertions (as HTMLSelectElement) will break. Use plain JS and narrow via instanceof.

Apply this diff:

 <script>
-  function initLanguagePicker() {
-    const select = document.getElementById('language-select') as HTMLSelectElement;
-    
-    if (select) {
-      // Remover event listeners previos para evitar duplicados
-      const newSelect = select.cloneNode(true) as HTMLSelectElement;
-      select.parentNode?.replaceChild(newSelect, select);
-      
-      newSelect.addEventListener('change', function() {
-        const selectedOption = this.options[this.selectedIndex];
-        const url = selectedOption.getAttribute('data-url');
-        if (url) {
-          window.location.href = url;
-        }
-      });
-    }
-  }
-  
-  // Inicializar cuando se carga la página
-  document.addEventListener('DOMContentLoaded', initLanguagePicker);
-  
-  // Reinicializar después de cada transición de página de Astro
-  document.addEventListener('astro:page-load', initLanguagePicker);
+  function initLanguagePicker() {
+    const select = document.getElementById('language-select');
+    if (select && select instanceof HTMLSelectElement) {
+      // Remove previous listeners by cloning
+      const newSelect = select.cloneNode(true);
+      select.parentNode?.replaceChild(newSelect, select);
+      newSelect.addEventListener('change', (e) => {
+        const target = e.currentTarget;
+        if (target && target instanceof HTMLSelectElement) {
+          const option = target.options[target.selectedIndex];
+          const url = option.getAttribute('data-url');
+          if (url) window.location.assign(url);
+        }
+      });
+    }
+  }
+  document.addEventListener('DOMContentLoaded', initLanguagePicker);
+  document.addEventListener('astro:page-load', initLanguagePicker);
 </script>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script>
function initLanguagePicker() {
const select = document.getElementById('language-select') as HTMLSelectElement;
if (select) {
// Remover event listeners previos para evitar duplicados
const newSelect = select.cloneNode(true) as HTMLSelectElement;
select.parentNode?.replaceChild(newSelect, select);
newSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
const url = selectedOption.getAttribute('data-url');
if (url) {
window.location.href = url;
}
});
}
}
// Inicializar cuando se carga la página
document.addEventListener('DOMContentLoaded', initLanguagePicker);
// Reinicializar después de cada transición de página de Astro
document.addEventListener('astro:page-load', initLanguagePicker);
</script>
<script>
function initLanguagePicker() {
const select = document.getElementById('language-select');
if (select && select instanceof HTMLSelectElement) {
// Remove previous listeners by cloning
const newSelect = select.cloneNode(true);
select.parentNode?.replaceChild(newSelect, select);
newSelect.addEventListener('change', (e) => {
const target = e.currentTarget;
if (target && target instanceof HTMLSelectElement) {
const option = target.options[target.selectedIndex];
const url = option.getAttribute('data-url');
if (url) window.location.assign(url);
}
});
}
}
document.addEventListener('DOMContentLoaded', initLanguagePicker);
document.addEventListener('astro:page-load', initLanguagePicker);
</script>
🤖 Prompt for AI Agents
In src/components/LanguagePicker.astro around lines 33 to 57, the inline script
uses TypeScript-only syntax (type assertions like "as HTMLSelectElement") which
will cause runtime errors in the browser; remove all TypeScript syntax and use
plain JavaScript: get the element with document.getElementById, check it with
instanceof HTMLSelectElement before using it, avoid TS casts on cloneNode (just
treat the result as an Element and guard with instanceof), and inside the change
handler reference the element via the captured newSelect variable or
event.target (narrowed with instanceof) to read options and data-url; keep the
DOMContentLoaded and astro:page-load listeners but ensure the script contains
only valid JS and proper instanceof checks.

50 changes: 50 additions & 0 deletions src/components/Navigation.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
import { getLangFromUrl, useTranslations } from '../i18n/utils';
import { getRelativeLocaleUrl } from 'astro:i18n';

const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);

const menuItems = [
{
path: '',
label: t('nav.home')
},
{
path: lang === 'es' ? 'sobre-nosotros' : 'about-us',
label: t('nav.about')
},
{
path: lang === 'es' ? 'contacto' : 'contact',
label: t('nav.contact')
},
{
path: lang === 'es' ? 'redes-sociales' : 'social-networks',
label: t('nav.social')
},
];
---

<nav class="fixed top-20px left-20px z-1000 bg-white/5 backdrop-blur-md rounded-lg p-4">
<ul class="flex flex-col gap-2">
{menuItems.map((item) => (
<li>
<a
href={getRelativeLocaleUrl(lang, item.path)}
class="text-white hover:text-yellow-400 transition-colors duration-200 block text-sm"
>
{item.label}
</a>
</li>
))}
</ul>
</nav>

<style>
nav {
position: fixed;
top: 20px;
left: 20px;
z-index: 999;
}
</style>
13 changes: 13 additions & 0 deletions src/i18n/react-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ui, defaultLang } from './ui';

export function useTranslationsReact(lang: keyof typeof ui) {
return function t(key: keyof typeof ui[typeof defaultLang]) {
return ui[lang][key] || ui[defaultLang][key];
}
}

export function getLangFromUrlReact(url: string) {
const [, lang] = url.split('/');
if (lang in ui) return lang as keyof typeof ui;
return defaultLang;
}
Comment on lines +9 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Make URL language extraction robust for absolute URLs.

url.split('/') breaks for absolute URLs (https://…). Parse pathname first.

-export function getLangFromUrlReact(url: string) {
-  const [, lang] = url.split('/');
-  if (lang in ui) return lang as keyof typeof ui;
-  return defaultLang;
-}
+export function getLangFromUrlReact(url: string) {
+  let pathname = url;
+  try {
+    // If absolute URL, extract pathname; otherwise assume it's already a path
+    if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
+      pathname = new URL(url).pathname;
+    }
+  } catch {
+    // noop; fallback to given string
+  }
+  const first = pathname.replace(/^\/+/, '').split('/')[0] ?? '';
+  if (first && first in ui) return first as keyof typeof ui;
+  return defaultLang;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getLangFromUrlReact(url: string) {
const [, lang] = url.split('/');
if (lang in ui) return lang as keyof typeof ui;
return defaultLang;
}
export function getLangFromUrlReact(url: string) {
let pathname = url;
try {
// If absolute URL, extract pathname; otherwise assume it's already a path
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
pathname = new URL(url).pathname;
}
} catch {
// noop; fallback to given string
}
const first = pathname.replace(/^\/+/, '').split('/')[0] ?? '';
if (first && first in ui) return first as keyof typeof ui;
return defaultLang;
}
🤖 Prompt for AI Agents
In src/i18n/react-utils.ts around lines 9 to 13, the current url.split('/')
approach fails for absolute URLs; instead parse the URL's pathname before
splitting — use the URL constructor when input looks like an absolute URL
(catching errors) or fallback to treating the input as a relative path, get
pathname (or the original string), trim leading/trailing slashes, split on '/'
and take the first segment as lang; then check if that lang exists in ui and
return it as keyof typeof ui or return defaultLang if not.

Loading