feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,120 @@
import Link from 'next/link';
export function Footer() {
return (
<footer className="mt-auto" style={{ backgroundColor: 'var(--color-footer-bg)', color: 'var(--color-footer-text)' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{/* Brand */}
<div className="col-span-2 md:col-span-1">
{/* FIX-04: Natural Almagro / Mercado de Vida stacked */}
<div className="mb-3">
<p
className="text-2xl font-bold leading-none"
style={{ fontFamily: 'var(--font-heading)', letterSpacing: '0.04em', color: 'var(--color-footer-text)' }}
>
Natural
</p>
<p
className="text-xs font-normal leading-tight mt-0.5"
style={{ fontFamily: 'var(--font-sans)', color: 'var(--color-footer-muted)', letterSpacing: '0.06em' }}
>
Mercado de Vida
</p>
</div>
<blockquote
className="pl-3 border-l-2 leading-relaxed text-sm italic"
style={{ borderLeftColor: 'var(--color-primary)', color: 'var(--color-footer-muted)' }}
>
Productos naturales y orgánicos para tu bienestar. Calidad certificada, envío a toda España.
</blockquote>
</div>
{/* Shop */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-widest mb-4" style={{ color: 'var(--color-footer-text)' }}>
Tienda
</h3>
<ul className="space-y-2">
{[
['/products', 'Productos'],
['/categories', 'Categorías'],
['/brands', 'Marcas'],
['/search', 'Buscar'],
].map(([href, label]) => (
<li key={href}>
<Link href={href}
className="text-sm transition-colors hover:opacity-80"
style={{ color: 'var(--color-footer-muted)' }}>
{label}
</Link>
</li>
))}
</ul>
</div>
{/* Company */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-widest mb-4" style={{ color: 'var(--color-footer-text)' }}>
Empresa
</h3>
<ul className="space-y-2">
{[
['/about', 'Quiénes somos'],
['/contact', 'Contacto'],
].map(([href, label]) => (
<li key={href}>
<Link href={href}
className="text-sm transition-colors hover:opacity-80"
style={{ color: 'var(--color-footer-muted)' }}>
{label}
</Link>
</li>
))}
</ul>
</div>
{/* Legal */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-widest mb-4" style={{ color: 'var(--color-footer-text)' }}>
Legal
</h3>
<ul className="space-y-2">
{[
['/shipping', 'Envíos'],
['/privacy', 'Privacidad'],
['/terms', 'Términos'],
['/cookies', 'Cookies'],
].map(([href, label]) => (
<li key={href}>
<Link href={href}
className="text-sm transition-colors hover:opacity-80"
style={{ color: 'var(--color-footer-muted)' }}>
{label}
</Link>
</li>
))}
</ul>
</div>
</div>
{/* Bottom bar */}
<div className="mt-10 pt-6 flex flex-col sm:flex-row justify-between items-center gap-4" style={{ borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<p className="text-xs" style={{ color: 'rgba(232,245,224,0.4)' }}>
© 2026 MercadoDeVida. Todos los derechos reservados.
</p>
<div className="flex items-center gap-4">
<a href="https://instagram.com/mercadodevida/" target="_blank" rel="noopener" aria-label="Instagram"
className="transition-opacity hover:opacity-70" style={{ color: 'var(--color-footer-muted)' }}>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
</a>
<a href="https://www.facebook.com/mercadodevida" target="_blank" rel="noopener" aria-label="Facebook"
className="transition-opacity hover:opacity-70" style={{ color: 'var(--color-footer-muted)' }}>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,246 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import CartLink from '@/components/cart/CartLink';
import UserMenu from '@/components/auth/UserMenu';
import { fetchProducts, fetchSearchSuggestions } from '@/lib/api';
interface SearchProduct {
id: string;
name: string;
slug: string;
images?: { url: string }[];
}
function LiveSearchResults({
q,
results,
suggestions,
onClose,
onSuggestionClick,
}: {
q: string;
results: SearchProduct[];
suggestions: string[];
onClose: () => void;
onSuggestionClick: (term: string) => void;
}) {
if (!q.trim()) return null;
const hasResults = results.length > 0;
return (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-xl shadow-lg z-50 overflow-hidden">
{results.slice(0, 6).map((product) => (
<Link
key={product.id}
href={`/products/${product.slug}`}
onClick={onClose}
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors"
>
<div className="w-10 h-10 bg-gray-100 rounded-lg overflow-hidden flex-shrink-0 flex items-center justify-center">
{product.images?.[0] ? (
<Image
src={product.images[0].url}
alt={product.name}
width={40}
height={40}
className="object-cover w-full h-full"
/>
) : (
<span className="text-lg">🌿</span>
)}
</div>
<span className="text-sm font-medium text-gray-800 line-clamp-1">{product.name}</span>
</Link>
))}
{!hasResults && (
<div className="px-4 py-3">
<p className="text-sm text-gray-400 mb-2">Sin resultados para &ldquo;{q}&rdquo;</p>
{suggestions.length > 0 && (
<div>
<p className="text-xs text-gray-400 mb-1">Quizás buscabas:</p>
<div className="flex flex-wrap gap-1">
{suggestions.map((s) => (
<button
key={s}
onClick={() => onSuggestionClick(s)}
className="text-xs px-2 py-1 bg-[#70ad47]/10 text-[#70ad47] rounded-full hover:bg-[#70ad47]/20 transition-colors font-medium"
>
{s}
</button>
))}
</div>
</div>
)}
</div>
)}
<Link
href={`/search?q=${encodeURIComponent(q)}`}
onClick={onClose}
className="block px-4 py-2.5 text-xs text-center text-[#70ad47] hover:bg-[#70ad47]/5 font-medium border-t border-gray-100 transition-colors"
>
Ver todos los resultados para &ldquo;{q}&rdquo;
</Link>
</div>
);
}
export function Header() {
const [q, setQ] = useState('');
const [results, setResults] = useState<SearchProduct[]>([]);
const [suggestions, setSuggestions] = useState<string[]>([]);
const [showDropdown, setShowDropdown] = useState(false);
const [searching, setSearching] = useState(false);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const loadResults = async (term: string) => {
setSearching(true);
try {
const [items, suggs] = await Promise.all([
fetchProducts({ q: term, limit: 6 }),
fetchSearchSuggestions(term),
]);
setResults(Array.isArray(items) ? items as SearchProduct[] : []);
setSuggestions(suggs);
setShowDropdown(true);
} catch {
setResults([]);
setSuggestions([]);
setShowDropdown(false);
} finally {
setSearching(false);
}
};
useEffect(() => {
if (!q.trim()) {
setResults([]);
setSuggestions([]);
setShowDropdown(false);
setSearching(false);
return;
}
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
loadResults(q.trim());
}, 300);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [q]);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setShowDropdown(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const handleSuggestionClick = (term: string) => {
setQ(term);
setShowDropdown(false);
router.push(`/search?q=${encodeURIComponent(term)}`);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setShowDropdown(false);
const term = q.trim();
if (term) router.push(`/search?q=${encodeURIComponent(term)}`);
else router.push('/search');
};
return (
<header className="sticky top-0 z-50 bg-white border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16 gap-4">
{/* Logo */}
<Link href="/" className="flex items-center shrink-0">
<Image
src="/images/logo-main.png"
alt="MercadoDeVida"
width={56}
height={56}
className="h-14 w-auto object-contain"
priority
/>
</Link>
{/* Search bar */}
<div className="flex-1 max-w-xl hidden sm:block relative" ref={dropdownRef}>
<form onSubmit={handleSubmit}>
<div className="relative w-full">
<input
type="search"
value={q}
onChange={(e) => setQ(e.target.value)}
onFocus={() => { if (q.trim() && (results.length > 0 || suggestions.length > 0)) setShowDropdown(true); }}
placeholder="Buscar productos, marcas..."
className="w-full pl-10 pr-10 py-2.5 border border-gray-200 rounded-full text-sm
focus:ring-2 focus:ring-[#70ad47] focus:border-transparent outline-none
placeholder-gray-400 text-gray-900 bg-gray-50 hover:bg-white transition-colors"
/>
<svg
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none"
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{searching && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<div className="w-4 h-4 border-2 border-gray-300 border-t-[#70ad47] rounded-full animate-spin" />
</div>
)}
</div>
</form>
{showDropdown && (
<LiveSearchResults
q={q}
results={results}
suggestions={suggestions}
onClose={() => setShowDropdown(false)}
onSuggestionClick={handleSuggestionClick}
/>
)}
</div>
{/* Nav */}
<nav className="hidden lg:flex items-center gap-6">
<Link href="/products" className="text-sm font-medium text-gray-700 hover:text-[#70ad47] transition-colors">
Productos
</Link>
<Link href="/categories" className="text-sm font-medium text-gray-700 hover:text-[#70ad47] transition-colors">
Categorías
</Link>
<Link href="/brands" className="text-sm font-medium text-gray-700 hover:text-[#70ad47] transition-colors">
Marcas
</Link>
</nav>
{/* Cart + user */}
<div className="flex items-center gap-3 shrink-0">
<Link href="/search" className="sm:hidden text-gray-500 hover:text-[#70ad47]">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</Link>
<CartLink />
<UserMenu />
</div>
</div>
</div>
</header>
);
}