feat(F-103): completed feature

This commit is contained in:
chattie
2026-08-21 07:55:18 +02:00
parent 5177a851aa
commit c07776822d
40 changed files with 886 additions and 156 deletions

View File

@@ -2,6 +2,7 @@ import Link from 'next/link';
import Image from 'next/image';
import type { Metadata } from 'next';
import { fetchBrandBySlug, fetchProducts, formatPrice } from '@/lib/api';
import { formatRichText } from '@/lib/format-rich-text';
interface Props {
params: Promise<{ slug: string }>;
@@ -92,8 +93,8 @@ export default async function BrandPage({ params }: Props) {
</h3>
{product.description && (
<div
className="text-gray-500 text-xs mt-1 line-clamp-2 prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }}
className="text-gray-500 text-xs mt-1 line-clamp-2 rich-text prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: formatRichText(product.description) }}
/>
)}
<div className="mt-3 pr-2">

View File

@@ -2,6 +2,7 @@ import Link from 'next/link';
import Image from 'next/image';
import type { Metadata } from 'next';
import { fetchCategoryBySlug, fetchProducts, fetchBrands } from '@/lib/api';
import { formatRichText } from '@/lib/format-rich-text';
interface Props {
params: Promise<{ slug: string }>;
@@ -13,7 +14,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.description ?? `${category.name} — Productos naturales y orgánicos en mercadodevida.`,
};
}
@@ -68,8 +69,8 @@ export default async function CategoryPage({ params }: Props) {
<h1 className="text-3xl font-bold text-gray-900" style={{ fontFamily: 'var(--font-heading)' }}>
{category.name}
</h1>
{category.seoDescription && (
<p className="mt-2 text-gray-600">{category.seoDescription}</p>
{(category.description || category.seoDescription) && (
<p className="mt-2 text-gray-600">{category.description || category.seoDescription}</p>
)}
<p className="mt-1 text-sm text-gray-500">{products.length} producto{products.length !== 1 ? 's' : ''}</p>
</div>
@@ -105,8 +106,8 @@ export default async function CategoryPage({ params }: Props) {
</h3>
{product.description && (
<div
className="text-gray-500 text-xs mt-1 line-clamp-2 prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }}
className="text-gray-500 text-xs mt-1 line-clamp-2 rich-text prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: formatRichText(product.description) }}
/>
)}
<div className="mt-3 pr-2">

View File

@@ -39,14 +39,18 @@ export default async function CategoriesPage() {
{tree.map((cat, i) => {
const emoji = cat.emoji ?? icons[cat.slug] ?? '📦';
const colorClass = cat.color ?? colors[i % colors.length];
const customColors = Boolean(cat.bgColor || cat.textColor);
return (
<Link key={cat.id} href={`/categories/${cat.slug}`} className="group block">
<div className={`relative overflow-hidden rounded-2xl bg-gradient-to-br ${colorClass} p-6 text-white min-h-[140px] flex flex-col justify-between`}>
<div
className={`relative overflow-hidden rounded-2xl ${customColors ? '' : `bg-gradient-to-br ${colorClass} text-white`} p-6 min-h-[140px] flex flex-col justify-between`}
style={customColors ? { backgroundColor: cat.bgColor ?? '#F1F8E9', color: cat.textColor ?? '#2D6A4F' } : undefined}
>
<div className="absolute top-4 right-4 text-5xl opacity-20">{emoji}</div>
<div>
<h2 className="text-xl font-bold group-hover:underline">{cat.name}</h2>
{cat.seoDescription && (
<p className="mt-1 text-sm text-white/80 line-clamp-2">{cat.seoDescription}</p>
<p className="mt-1 text-sm opacity-80 line-clamp-2">{cat.description || cat.seoDescription}</p>
)}
</div>
<div className="mt-4 flex flex-wrap gap-2">

View File

@@ -33,3 +33,29 @@ body {
color: var(--foreground);
font-family: var(--font-sans), system-ui, sans-serif;
}
/* Texto enriquecido de descripciones generadas por IA o editadas en el admin */
.rich-text p {
margin: 0 0 0.75rem;
}
.rich-text p:last-child {
margin-bottom: 0;
}
.rich-text ul,
.rich-text ol {
margin: 0 0 0.75rem;
padding-left: 1.25rem;
}
.rich-text ul {
list-style: disc;
}
.rich-text ol {
list-style: decimal;
}
.rich-text li {
margin-bottom: 0.25rem;
}
.rich-text strong {
font-weight: 600;
color: inherit;
}

View File

@@ -13,6 +13,7 @@ import {
} from '@/lib/api';
import ProductAddToCart from '@/components/cart/ProductAddToCart';
import ProductAttributes from '@/components/product/ProductAttributes';
import { formatRichText } from '@/lib/format-rich-text';
interface Props {
params: Promise<{ slug: string }>;
@@ -199,8 +200,8 @@ export default async function ProductPage({ params }: Props) {
<div className="mt-8">
<h2 className="text-lg font-semibold text-gray-900 mb-2">Descripción</h2>
<div
className="text-gray-600 leading-relaxed prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }}
className="text-gray-600 leading-relaxed rich-text prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: formatRichText(product.description) }}
/>
</div>
)}

View File

@@ -2,6 +2,7 @@ import Link from 'next/link';
import Image from 'next/image';
import type { Metadata } from 'next';
import { fetchProducts, fetchBrands, fetchCategories, formatPrice } from '@/lib/api';
import { formatRichText } from '@/lib/format-rich-text';
export const metadata: Metadata = {
title: 'Productos — mercadodevida',
@@ -73,8 +74,8 @@ export default async function ProductsPage() {
</h3>
{product.description && (
<div
className="text-gray-500 text-xs mt-1 line-clamp-2 prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }}
className="text-gray-500 text-xs mt-1 line-clamp-2 rich-text prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: formatRichText(product.description) }}
/>
)}
<div className="mt-3 pr-2">

View File

@@ -2,6 +2,7 @@ import Link from 'next/link';
import Image from 'next/image';
import type { Metadata } from 'next';
import { fetchProducts, fetchBrands, fetchCategories, formatPrice } from '@/lib/api';
import { formatRichText } from '@/lib/format-rich-text';
interface Props {
searchParams: Promise<{ q?: string; brand?: string; category?: string }>;
@@ -108,8 +109,8 @@ export default async function SearchPage({ searchParams }: Props) {
</h3>
{product.description && (
<div
className="text-gray-500 text-xs mt-1 line-clamp-2 prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }}
className="text-gray-500 text-xs mt-1 line-clamp-2 rich-text prose prose-xs max-w-none"
dangerouslySetInnerHTML={{ __html: formatRichText(product.description) }}
/>
)}
<div className="mt-3 pr-2">