feat(F-048): completed feature

This commit is contained in:
chattie
2026-08-19 07:17:14 +02:00
parent 8ee1938af9
commit 835ab66eda
187 changed files with 12361 additions and 1065 deletions

6895
project/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,12 @@ import { fetchPage } from '@/lib/api';
export const metadata: Metadata = {
title: 'Quiénes somos',
description: 'Conoce la historia, misión y valores de MercadoDeVida. Productos naturales y orgánicos de confianza.',
description: 'Conoce la historia, misión y valores de mercadodevida. Productos naturales y orgánicos de confianza.',
};
const FALLBACK_HTML = `
<h2>Nuestra historia</h2>
<p>MercadoDeVida nació de la convicción de que comer bien no debería ser complicado. Seleccionamos cuidadosamente cada producto para asegurar la máxima calidad y transparencia.</p>
<p>mercadodevida nació de la convicción de que comer bien no debería ser complicado. Seleccionamos cuidadosamente cada producto para asegurar la máxima calidad y transparencia.</p>
<h2>Nuestra misión</h2>
<p>Facilitar el acceso a productos naturales y orgánicos de alta calidad, directamente desde productores certificados, sin intermediarios.</p>
<h2>Valores</h2>
@@ -31,7 +31,7 @@ export default async function AboutPage() {
return (
<ContentPage
title={cms?.title ?? 'Quiénes somos'}
description="Conoce la historia, misión y valores de MercadoDeVida. Productos naturales y orgánicos de confianza."
description="Conoce la historia, misión y valores de mercadodevida. Productos naturales y orgánicos de confianza."
>
<div dangerouslySetInnerHTML={{ __html: body }} />
</ContentPage>

View File

@@ -211,7 +211,10 @@ export default function AccountPage() {
}, [user]);
useEffect(() => {
load();
const timer = window.setTimeout(() => {
void load();
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
const handleDelete = async (id: string) => {

View File

@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Admin Pedidos — MercadoDeVida',
title: 'Admin Pedidos — mercadodevida',
};
export default function AdminOrdersPage() {

View File

@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: 'Admin — MercadoDeVida',
title: 'Admin — mercadodevida',
};
export default function AdminDashboard() {

View File

@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import { fetchProducts } from '@/lib/api';
export const metadata: Metadata = {
title: 'Admin Productos — MercadoDeVida',
title: 'Admin Productos — mercadodevida',
};
export default async function AdminProductsPage() {

View File

@@ -13,7 +13,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
if (!brand) return { title: 'Marca no encontrada' };
return {
title: brand.seoTitle ?? brand.name,
description: brand.seoDescription ?? `Productos ${brand.name} en MercadoDeVida.`,
description: brand.seoDescription ?? `Productos ${brand.name} en mercadodevida.`,
};
}

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
import { fetchBrands } from '@/lib/api';
export const metadata: Metadata = {
title: 'Marcas — MercadoDeVida',
title: 'Marcas — mercadodevida',
description: 'Todas las marcas de productos naturales y ecológicos.',
};

View File

@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import CartPageContent from '@/components/cart/CartPageContent';
export const metadata: Metadata = {
title: 'Carrito — MercadoDeVida',
title: 'Carrito — mercadodevida',
};
export default function CartPage() {

View File

@@ -13,7 +13,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
if (!category) return { title: 'Categoría no encontrada' };
return {
title: category.seoTitle ?? category.name,
description: category.seoDescription ?? `${category.name} — Productos naturales y orgánicos en MercadoDeVida.`,
description: category.seoDescription ?? `${category.name} — Productos naturales y orgánicos en mercadodevida.`,
};
}

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
import { fetchCategories } from '@/lib/api';
export const metadata: Metadata = {
title: 'Categorías — MercadoDeVida',
title: 'Categorías — mercadodevida',
description: 'Explora todas las categorías de productos naturales y orgánicos.',
};

View File

@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import CheckoutClient from '@/components/checkout/CheckoutClient';
export const metadata: Metadata = {
title: 'Checkout — MercadoDeVida',
title: 'Checkout — mercadodevida',
};
export default function CheckoutPage() {

View File

@@ -4,7 +4,7 @@ import { fetchPage } from '@/lib/api';
export const metadata: Metadata = {
title: 'Contacto',
description: 'Ponte en contacto con el equipo de MercadoDeVida. Resolvemos tus dudas sobre productos, pedidos y envíos.',
description: 'Ponte en contacto con el equipo de mercadodevida. Resolvemos tus dudas sobre productos, pedidos y envíos.',
};
const FALLBACK_HTML = `

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Política de cookies',
description: 'Información sobre el uso de cookies en MercadoDeVida.',
description: 'Información sobre el uso de cookies en mercadodevida.',
robots: { index: false, follow: false },
};

View File

@@ -13,14 +13,14 @@ const opensans = Open_Sans({
});
export const metadata: Metadata = {
title: 'MercadoDeVida — Productos naturales y orgánicos',
title: 'mercadodevida — Productos naturales y orgánicos',
description:
'Tienda online de productos naturales, orgánicos y saludables. Envío a toda España. Calidad certificada.',
icons: {
icon: '/images/favicon.png',
},
openGraph: {
title: 'MercadoDeVida — Productos naturales y orgánicos',
title: 'mercadodevida — Productos naturales y orgánicos',
description: 'Tienda online de productos naturales, orgánicos y saludables.',
type: 'website',
},

View File

@@ -9,8 +9,8 @@ export async function generateMetadata({ searchParams }: Props): Promise<Metadat
const { orderId } = await searchParams;
return {
title: orderId
? `Pedido ${orderId.slice(0, 8).toUpperCase()} confirmado — MercadoDeVida`
: 'Pedido confirmado — MercadoDeVida',
? `Pedido ${orderId.slice(0, 8).toUpperCase()} confirmado — mercadodevida`
: 'Pedido confirmado — mercadodevida',
};
}

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Política de privacidad',
description: 'Información sobre cómo MercadoDeVida recopila, usa y protege tus datos personales.',
description: 'Información sobre cómo mercadodevida recopila, usa y protege tus datos personales.',
robots: { index: false, follow: false },
};
@@ -20,7 +20,7 @@ export default function PrivacyPage() {
<h2>Responsable del tratamiento</h2>
<p>
MercadoDeVida<br />
mercadodevida<br />
Email: hola@mercadodevida.es
</p>

View File

@@ -4,7 +4,7 @@ import type { Metadata } from 'next';
import { fetchProducts, fetchBrands, fetchCategories, formatPrice } from '@/lib/api';
export const metadata: Metadata = {
title: 'Productos — MercadoDeVida',
title: 'Productos — mercadodevida',
description: 'Todos los productos naturales y orgánicos.',
};

View File

@@ -10,7 +10,7 @@ interface Props {
export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
const { q } = await searchParams;
const title = q ? `Buscar: "${q}"` : 'Buscar productos';
return { title, description: `${title} en MercadoDeVida.` };
return { title, description: `${title} en mercadodevida.` };
}
export default async function SearchPage({ searchParams }: Props) {

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Términos y condiciones',
description: 'Condiciones generales de venta de MercadoDeVida. Lea atentamente antes de realizar su pedido.',
description: 'Condiciones generales de venta de mercadodevida. Lea atentamente antes de realizar su pedido.',
robots: { index: false, follow: false },
};
@@ -20,7 +20,7 @@ export default function TermsPage() {
<h2>Identificación del vendedor</h2>
<p>
MercadoDeVida<br />
mercadodevida<br />
Email: hola@mercadodevida.es
</p>

View File

@@ -101,7 +101,7 @@ export function Footer() {
{/* 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.
© 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"

View File

@@ -120,13 +120,7 @@ export function Header() {
};
useEffect(() => {
if (!q.trim()) {
setResults([]);
setSuggestions([]);
setShowDropdown(false);
setSearching(false);
return;
}
if (!q.trim()) return;
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
loadResults(q.trim());
@@ -146,6 +140,16 @@ export function Header() {
return () => document.removeEventListener('mousedown', handler);
}, []);
const handleSearchChange = (value: string) => {
setQ(value);
if (!value.trim()) {
setResults([]);
setSuggestions([]);
setShowDropdown(false);
setSearching(false);
}
};
const handleSuggestionClick = (term: string) => {
setQ(term);
setShowDropdown(false);
@@ -168,7 +172,7 @@ export function Header() {
<Link href="/" className="flex items-center shrink-0">
<Image
src="/images/logo-main.png"
alt="MercadoDeVida"
alt="mercadodevida"
width={56}
height={56}
className="h-14 w-auto object-contain"
@@ -183,7 +187,7 @@ export function Header() {
<input
type="search"
value={q}
onChange={(e) => setQ(e.target.value)}
onChange={(e) => handleSearchChange(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

View File

@@ -26,15 +26,13 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
useEffect(() => {
try {
const stored = localStorage.getItem('mdv_cart');
if (stored) setItems(JSON.parse(stored));
} catch {}
}, []);
const persist = useCallback((newItems: CartItem[]) => {
setItems(newItems);
localStorage.setItem('mdv_cart', JSON.stringify(newItems));
const timer = window.setTimeout(() => {
try {
const stored = localStorage.getItem('mdv_cart');
if (stored) setItems(JSON.parse(stored));
} catch {}
}, 0);
return () => window.clearTimeout(timer);
}, []);
const addItem = useCallback((item: CartItem) => {