Feat: Cambios Varios 2

This commit is contained in:
2026-01-05 10:30:04 -03:00
parent 8bc1308bc5
commit 0fa77e4a98
184 changed files with 11098 additions and 6348 deletions

View File

@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { wizardService } from '../../services/wizardService';
import { useWizardStore } from '../../store/wizardStore';
import type { AttributeDefinition } from '../../types';
import { StepWrapper } from '../../components/StepWrapper';
import { ChevronRight, Info, Tag, PenTool, DollarSign } from 'lucide-react';
export default function AttributeForm() {
const { selectedCategory, selectedOperation, attributes, setAttribute, setStep } = useWizardStore();
const [definitions, setDefinitions] = useState<AttributeDefinition[]>([]);
useEffect(() => {
// CAMBIO: Verificamos específicamente que exista el ID para evitar el GET /undefined
if (selectedCategory?.id) {
wizardService.getAttributes(selectedCategory.id)
.then(setDefinitions)
.catch(err => console.error("Error al obtener atributos:", err));
}
}, [selectedCategory?.id]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setStep(4);
};
return (
<StepWrapper>
{/* Breadcrumbs */}
<div className="flex items-center gap-3 mb-8 px-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400">
<button onClick={() => setStep(1)} className="hover:text-blue-600 transition-colors">
{selectedCategory?.name}
</button>
<ChevronRight size={10} className="opacity-30" />
<button onClick={() => setStep(2)} className="hover:text-blue-600 transition-colors">
{selectedOperation?.name}
</button>
</div>
</div>
<div className="mb-10">
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase leading-none">
Características <br />
<span className="text-blue-600">del Anuncio</span>
</h2>
<p className="text-slate-400 text-sm mt-3 font-medium">Completa los datos técnicos para mejorar la visibilidad.</p>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
{/* GRUPO: INFORMACIÓN BÁSICA */}
<div className="space-y-6 bg-white p-8 rounded-[2.5rem] border border-slate-100 shadow-sm">
<div className="flex items-center gap-2 text-blue-600 mb-2">
<PenTool size={16} />
<span className="text-[10px] font-black uppercase tracking-[0.2em]">Información General</span>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Título del Aviso</label>
<input
type="text"
required
className="w-full p-4 bg-slate-50 border-2 border-slate-100 rounded-2xl outline-none focus:border-blue-500 focus:bg-white transition-all font-bold text-slate-700 text-sm"
onChange={(e) => setAttribute('title', e.target.value)}
value={attributes['title'] || ''}
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Precio de Venta</label>
<div className="relative flex items-center">
{/* Icono de moneda separado del texto */}
<div className="absolute left-4 text-slate-400 font-black text-lg pointer-events-none">
<DollarSign size={18} />
</div>
<input
type="number"
required
/* Aumentamos pl-12 para evitar superposición */
className="w-full p-4 pl-12 bg-slate-50 border-2 border-slate-100 rounded-2xl outline-none focus:border-blue-500 focus:bg-white transition-all font-mono font-black text-slate-800 text-lg"
onChange={(e) => setAttribute('price', e.target.value)}
value={attributes['price'] || ''}
/>
</div>
</div>
</div>
{/* GRUPO: ESPECIFICACIONES TÉCNICAS */}
{definitions.length > 0 && (
<div className="space-y-6 bg-slate-50/50 p-8 rounded-[2.5rem] border-2 border-slate-100">
<div className="flex items-center gap-2 text-slate-500 mb-2">
<Tag size={16} />
<span className="text-[10px] font-black uppercase tracking-[0.2em]">Ficha Técnica</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{definitions.map(def => (
<div key={def.id} className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">
{def.name} {def.required && <span className="text-rose-500">*</span>}
</label>
<input
type={def.dataType === 'number' ? 'number' : 'text'}
required={def.required}
placeholder={`Ingrese ${def.name.toLowerCase()}...`}
className="w-full p-4 bg-white border-2 border-slate-100 rounded-2xl outline-none focus:border-blue-500 transition-all font-bold text-slate-700 text-sm placeholder:text-slate-200 shadow-sm"
value={attributes[def.name.toLowerCase()] || ''}
onChange={(e) => setAttribute(def.name.toLowerCase(), e.target.value)}
/>
</div>
))}
</div>
</div>
)}
<div className="p-6 bg-blue-50 rounded-3xl flex gap-4 border border-blue-100 items-start">
<Info className="text-blue-500 shrink-0" size={20} />
<p className="text-[10px] text-blue-700 font-bold leading-relaxed uppercase opacity-80">
Los datos cargados aquí sirven para filtrar las búsquedas en el portal digital.
</p>
</div>
{/* BOTÓN CON ALTO CONTRASTE */}
<button
type="submit"
className="w-full bg-slate-950 hover:bg-black text-white font-black uppercase text-xs tracking-[0.2em] py-6 rounded-[2rem] shadow-2xl shadow-slate-300 transition-all mt-6 active:scale-[0.98] flex items-center justify-center gap-3 group"
>
Continuar al Editor de Texto
<ChevronRight size={18} className="group-hover:translate-x-1 transition-transform" />
</button>
</form>
</StepWrapper>
);
}

View File

@@ -0,0 +1,63 @@
import { useEffect, useState } from 'react';
import { wizardService } from '../../services/wizardService';
import { useWizardStore } from '../../store/wizardStore';
import type { Category } from '../../types'; // ID: type-import-fix
import { StepWrapper } from '../../components/StepWrapper';
import { ChevronRight } from 'lucide-react';
export default function CategoryParams() {
const [categories, setCategories] = useState<Category[]>([]);
const [navStack, setNavStack] = useState<Category[]>([]); // For hierarchical navigation
const setCategory = useWizardStore(state => state.setCategory);
useEffect(() => {
wizardService.getCategories().then(setCategories);
}, []);
// Filter categories dependent on current parent
const currentParentId = navStack.length > 0 ? navStack[navStack.length - 1].id : null;
const displayedCategories = categories.filter(c => c.parentId === currentParentId);
const handleSelect = (category: Category) => {
const hasChildren = categories.some(c => c.parentId === category.id);
if (hasChildren) {
setNavStack([...navStack, category]);
} else {
setCategory(category);
}
};
const handleBack = () => {
if (navStack.length > 0) {
setNavStack(navStack.slice(0, -1));
}
};
return (
<StepWrapper>
<h2 className="text-2xl font-bold mb-6 text-brand-900">¿Qué deseas publicar?</h2>
{navStack.length > 0 && (
<button
onClick={handleBack}
className="mb-4 text-sm text-brand-600 hover:text-brand-800 flex items-center gap-1"
>
&larr; Volver a {navStack.length > 1 ? navStack[navStack.length - 2].name : 'Inicio'}
</button>
)}
<div className="grid gap-3">
{displayedCategories.map(cat => (
<button
key={cat.id}
onClick={() => handleSelect(cat)}
className="group flex items-center justify-between p-4 bg-white border border-slate-200 rounded-lg shadow-sm hover:border-brand-500 hover:ring-1 hover:ring-brand-500 transition-all text-left"
>
<span className="font-medium text-slate-700 group-hover:text-brand-700">{cat.name}</span>
<ChevronRight className="text-slate-400 group-hover:text-brand-500" />
</button>
))}
</div>
</StepWrapper>
);
}

View File

@@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
import { wizardService } from '../../services/wizardService';
import { useWizardStore } from '../../store/wizardStore';
import type { Operation } from '../../types';
import { StepWrapper } from '../../components/StepWrapper';
export default function OperationSelection() {
const { selectedCategory, setOperation, setStep } = useWizardStore();
const [operations, setOperations] = useState<Operation[]>([]);
useEffect(() => {
if (selectedCategory) {
wizardService.getOperations(selectedCategory.id).then(setOperations);
}
}, [selectedCategory]);
return (
<StepWrapper>
<button onClick={() => setStep(1)} className="mb-4 text-brand-600 font-medium text-sm">Cambiar Categoría</button>
<h2 className="text-2xl font-bold mb-2 text-brand-900">Tipo de Operación</h2>
<p className="text-slate-500 mb-6">Seleccionaste: <span className="font-semibold text-brand-700">{selectedCategory?.name}</span></p>
<div className="grid grid-cols-2 gap-4">
{operations.length === 0 ? (
<div className="col-span-2 text-center py-8 bg-white rounded border border-yellow-200 bg-yellow-50 text-yellow-800">
No hay operaciones disponibles para este rubro.
</div>
) : (
operations.map(op => (
<button
key={op.id}
onClick={() => setOperation(op)}
className="p-6 bg-white border border-slate-200 rounded-xl hover:border-brand-500 hover:bg-brand-50 hover:text-brand-700 transition font-semibold text-lg shadow-sm"
>
{op.name}
</button>
))
)}
</div>
</StepWrapper>
);
}

View File

@@ -0,0 +1,107 @@
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { StepWrapper } from '../../components/StepWrapper';
import { useWizardStore } from '../../store/wizardStore';
import { Upload, Plus, Trash2, ArrowLeft, ChevronRight } from 'lucide-react';
import clsx from 'clsx';
export default function PhotoUploadStep() {
const { setStep, photos, existingImages, removePhoto, removeExistingImage, addPhoto } = useWizardStore();
const baseUrl = import.meta.env.VITE_BASE_URL;
const onDrop = useCallback((acceptedFiles: File[]) => {
acceptedFiles.forEach(file => addPhoto(file));
}, [addPhoto]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { 'image/*': [] },
maxFiles: 10
});
return (
<StepWrapper>
<div className="mb-10 text-center">
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase leading-none">
Galería de <br />
<span className="text-blue-600">Imágenes</span>
</h2>
<p className="text-slate-400 text-sm mt-3 font-medium">Gestiona las fotos de tu aviso. La primera será la portada.</p>
</div>
<div className="space-y-10">
{/* GRILLA DE FOTOS (EXISTENTES + NUEVAS) */}
{(existingImages.length > 0 || photos.length > 0) && (
<section className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Renderizado de Fotos Heredadas */}
{existingImages.map((img, idx) => (
<div key={`old-${idx}`} className="group relative aspect-square bg-slate-100 rounded-[2rem] overflow-hidden border-2 border-slate-100 shadow-sm transition-all hover:shadow-xl">
<img src={`${baseUrl}${img.url}`} className="w-full h-full object-cover" alt="Original" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button onClick={() => removeExistingImage(idx)} className="bg-rose-500 text-white p-3 rounded-2xl hover:bg-rose-600 transition-colors shadow-lg">
<Trash2 size={20} />
</button>
</div>
{idx === 0 && (
<div className="absolute top-3 left-3 bg-blue-600 text-white text-[8px] font-black uppercase px-2 py-1 rounded-lg shadow-lg">Portada</div>
)}
</div>
))}
{/* Renderizado de Fotos Nuevas (Files) */}
{photos.map((file, idx) => (
<div key={`new-${idx}`} className="group relative aspect-square bg-blue-50 rounded-[2rem] overflow-hidden border-2 border-blue-200 shadow-sm transition-all hover:shadow-xl">
<img src={URL.createObjectURL(file)} className="w-full h-full object-cover" alt="Nueva" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button onClick={() => removePhoto(idx)} className="bg-rose-500 text-white p-3 rounded-2xl shadow-lg">
<Trash2 size={20} />
</button>
</div>
{existingImages.length === 0 && idx === 0 && (
<div className="absolute top-3 left-3 bg-blue-600 text-white text-[8px] font-black uppercase px-2 py-1 rounded-lg shadow-lg">Portada</div>
)}
</div>
))}
{/* BOTÓN "AÑADIR MÁS" dentro de la grilla si ya hay fotos */}
<div {...getRootProps()} className="aspect-square bg-slate-50 border-2 border-dashed border-slate-200 rounded-[2rem] flex flex-col items-center justify-center cursor-pointer hover:bg-white hover:border-blue-400 transition-all group">
<input {...getInputProps()} />
<div className="p-3 bg-white rounded-2xl shadow-sm text-slate-400 group-hover:text-blue-500 group-hover:shadow-blue-100 transition-all">
<Plus size={24} />
</div>
<span className="text-[9px] font-black text-slate-400 uppercase mt-2 group-hover:text-blue-600">Añadir más</span>
</div>
</section>
)}
{/* ÁREA DE CARGA INICIAL (Solo si está todo vacío) */}
{existingImages.length === 0 && photos.length === 0 && (
<section {...getRootProps()} className={clsx(
"p-16 border-4 border-dashed rounded-[3rem] text-center transition-all cursor-pointer group",
isDragActive ? "border-blue-500 bg-blue-50" : "border-slate-100 bg-white hover:border-slate-200 hover:bg-slate-50"
)}>
<input {...getInputProps()} />
<div className="w-20 h-20 bg-blue-50 text-blue-600 rounded-[2rem] flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform">
<Upload size={32} />
</div>
<h3 className="text-xl font-black text-slate-900 uppercase tracking-tighter mb-2">Sube tus fotografías</h3>
<p className="text-slate-400 text-xs font-medium max-w-xs mx-auto leading-relaxed">
Arrastra tus fotos aquí o haz clic para buscarlas. Soporta JPG, PNG y WebP.
</p>
</section>
)}
<div className="flex gap-4 pt-6">
<button onClick={() => setStep(4)} className="flex-1 py-5 bg-slate-100 text-slate-500 font-black uppercase text-xs rounded-2xl hover:bg-slate-200 transition-all flex items-center justify-center gap-3">
<ArrowLeft size={16} /> Volver
</button>
<button onClick={() => setStep(6)} className="flex-[2] py-5 bg-blue-600 text-white font-black uppercase text-xs rounded-2xl shadow-xl shadow-blue-200 hover:bg-blue-700 transition-all flex items-center justify-center gap-3">
Continuar al Resumen <ChevronRight size={18} />
</button>
</div>
</div>
</StepWrapper>
);
}

View File

@@ -0,0 +1,291 @@
import { useState } from 'react';
import { useWizardStore } from '../../store/wizardStore';
import { wizardService } from '../../services/wizardService';
import { StepWrapper } from '../../components/StepWrapper';
import type { AttributeDefinition } from '../../types';
import {
CreditCard,
Wallet,
ChevronRight,
ArrowLeft,
Tag,
FileText,
Clock
} from 'lucide-react';
import { initMercadoPago, Wallet as MPWallet } from '@mercadopago/sdk-react';
import clsx from 'clsx';
import api from '../../services/api';
// Se inicializa con la Public Key desde env
initMercadoPago(import.meta.env.VITE_MP_PUBLIC_KEY);
export default function SummaryStep({ definitions }: { definitions: AttributeDefinition[] }) {
const { selectedCategory, selectedOperation, attributes, photos, setStep, reset } = useWizardStore();
const [isSubmitting, setIsSubmitting] = useState(false);
const [preferenceId, setPreferenceId] = useState<string | null>(null);
const [createdId, setCreatedId] = useState<number | null>(null);
const [paymentMethod, setPaymentMethod] = useState<'mercadopago' | 'stripe'>('mercadopago');
const adFee = parseFloat(attributes['adFee'] || "0");
const listingPrice = parseFloat(attributes['price'] || "0");
const handlePublish = async () => {
if (!selectedCategory || !selectedOperation) return;
setIsSubmitting(true);
try {
const attributePayload: Record<number, string> = {};
const { existingImages } = useWizardStore.getState();
definitions.forEach(def => {
const val = attributes[def.name.toLowerCase()] || attributes[def.name];
if (val) {
attributePayload[def.id] = val.toString();
}
});
const payload = {
categoryId: selectedCategory.id,
operationId: selectedOperation.id,
title: attributes['title'],
description: attributes['description'],
price: parseFloat(attributes['price'] || "0"),
adFee: adFee,
currency: 'ARS',
status: 'Pending',
origin: 'Web',
attributes: attributePayload,
imagesToClone: existingImages.map(img => img.url),
printText: attributes['description'],
printDaysCount: parseInt(attributes['days'] || "3"),
isBold: false,
isFrame: false,
printFontSize: 'normal',
printAlignment: 'left'
};
const result = await wizardService.createListing(payload);
setCreatedId(result.id);
if (photos.length > 0) {
for (const photo of photos) {
await wizardService.uploadImage(result.id, photo);
}
}
const resp = await fetch(`${import.meta.env.VITE_API_URL}/payments/create-preference/${result.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(adFee)
});
const data = await resp.json();
if (data.id) {
setPreferenceId(data.id);
}
} catch (error) {
console.error(error);
alert('Error al publicar');
} finally {
setIsSubmitting(false);
}
};
const urlParams = new URLSearchParams(window.location.search);
const paymentStatus = urlParams.get('status');
if (paymentStatus === 'approved') {
return (
<StepWrapper>
<div className="text-center py-20 bg-white rounded-[3rem] shadow-2xl p-12 border border-emerald-100">
<div className="w-24 h-24 bg-blue-50 text-blue-500 rounded-full flex items-center justify-center mx-auto mb-8 shadow-inner">
<Clock size={64} strokeWidth={1.5} className="animate-pulse" />
</div>
<h2 className="text-4xl font-black mb-4 text-slate-900 tracking-tighter uppercase">¡Pago Recibido!</h2>
<p className="text-slate-500 text-lg mb-10 font-medium max-w-md mx-auto">
Tu aviso ha sido enviado a nuestro equipo de **Moderación**. Una vez aprobado, se publicará automáticamente en el portal y en la edición impresa.
</p>
<div className="flex flex-col gap-3 max-w-xs mx-auto">
<button
onClick={() => { reset(); window.location.href = '/profile'; }}
className="bg-slate-900 text-white px-12 py-5 rounded-2xl font-black uppercase text-xs tracking-[0.2em] hover:bg-black transition-all shadow-xl shadow-slate-200"
>
Ver estado en mi Perfil
</button>
</div>
</div>
</StepWrapper>
);
}
if (preferenceId) {
return (
<StepWrapper>
<div className="text-center py-20 bg-white rounded-[3rem] shadow-2xl p-12 border border-blue-100">
<div className="w-24 h-24 bg-blue-50 text-blue-600 rounded-full flex items-center justify-center mx-auto mb-8">
<Wallet size={56} strokeWidth={1.5} />
</div>
<h2 className="text-3xl font-black mb-4 text-slate-900 tracking-tighter uppercase">Finalizar Publicación</h2>
{/* LÓGICA DE SIMULACIÓN */}
{preferenceId === "MOCK_PREFERENCE_ID_12345" ? (
<div className="space-y-6">
<div className="p-4 bg-amber-50 border border-amber-100 rounded-2xl text-amber-700 text-xs font-bold uppercase">
Modo Simulación Activo: Token de MP no configurado.
</div>
<p className="text-slate-500 text-sm mb-8 font-medium max-w-sm mx-auto">
Estás en el entorno de desarrollo. Haz clic abajo para simular un pago aprobado y finalizar el proceso.
</p>
<button
onClick={async () => {
try {
// Enviamos un objeto en lugar del número solo
await api.post(`/payments/confirm-simulation/${createdId}`, {
amount: adFee
});
// Si la petición es exitosa, procedemos a la pantalla de éxito
window.location.href = window.location.pathname + '?status=approved';
} catch (error) {
console.error("Error en la simulación:", error);
alert("No se pudo confirmar el pago simulado.");
}
}}
className="w-full py-5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-2xl font-black uppercase text-xs tracking-widest shadow-xl shadow-emerald-200 transition-all"
>
Simular Pago Aprobado
</button>
</div>
) : (
// LÓGICA REAL
<>
<p className="text-slate-500 text-lg mb-10 font-medium max-w-sm mx-auto leading-tight">
Haz clic abajo para completar el pago de <strong>${adFee.toLocaleString()}</strong> de forma segura con Mercado Pago.
</p>
<div className="max-w-xs mx-auto scale-110">
<MPWallet initialization={{ preferenceId }} />
</div>
</>
)}
</div>
</StepWrapper>
);
}
return (
<StepWrapper>
<div className="mb-10 text-center">
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400 mb-4 block">Paso Final</span>
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase leading-none">
Resumen de <br />
<span className="text-blue-600">tu publicación</span>
</h2>
</div>
<div className="max-w-2xl mx-auto space-y-8">
{/* RESUMEN DEL AVISO */}
<section className="bg-white p-8 rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100">
<div className="flex justify-between items-start mb-8 pb-6 border-b border-slate-50">
<div className="flex items-center gap-4">
<div className="p-3 bg-slate-50 rounded-2xl text-slate-400"><FileText size={20} /></div>
<div>
<h3 className="font-black text-slate-900 uppercase text-sm tracking-tight">{attributes['title']}</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{selectedCategory?.name} / {selectedOperation?.name}</p>
</div>
</div>
<div className="text-right">
<span className="text-[9px] font-black text-slate-400 uppercase block mb-1">Precio Producto</span>
<p className="text-xl font-black text-slate-900">${listingPrice.toLocaleString()}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-x-10 gap-y-4 mb-4">
{definitions.map(def => {
const val = attributes[def.name.toLowerCase()] || attributes[def.name];
return val && (
<div key={def.id} className="flex justify-between items-center group">
<span className="text-[10px] font-bold text-slate-400 uppercase group-hover:text-slate-600 transition-colors">{def.name}</span>
<span className="text-xs font-black text-slate-800 uppercase">{val}</span>
</div>
);
})}
</div>
</section>
{/* --- BLOQUE DE COSTO (TOTAL A PAGAR) --- */}
<section className="bg-blue-600 rounded-[2.5rem] p-8 text-white shadow-2xl shadow-blue-200 relative overflow-hidden">
<div className="flex justify-between items-center relative z-10">
<div>
<span className="text-[10px] font-black uppercase tracking-[0.3em] opacity-60">Total a Pagar</span>
<div className="flex items-baseline gap-2 mt-1">
<span className="text-xl font-black">$</span>
<span className="text-5xl font-black font-mono tracking-tighter">
{adFee.toLocaleString('es-AR', { minimumFractionDigits: 2 })}
</span>
</div>
</div>
<div className="bg-white/10 p-4 rounded-[1.5rem] border border-white/20 backdrop-blur-sm">
<Tag size={24} className="text-white" />
</div>
</div>
</section>
{/* MÉTODO DE PAGO */}
<section className="space-y-4">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-widest ml-4">Selecciona Método de Pago</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className={clsx(
"flex items-center gap-4 p-6 rounded-3xl border-2 cursor-pointer transition-all",
paymentMethod === 'mercadopago' ? "border-blue-600 bg-blue-50 shadow-lg shadow-blue-100" : "border-slate-100 bg-white hover:border-blue-200"
)}>
<input type="radio" className="hidden" checked={paymentMethod === 'mercadopago'} onChange={() => setPaymentMethod('mercadopago')} />
<div className={clsx("p-3 rounded-2xl", paymentMethod === 'mercadopago' ? "bg-blue-600 text-white" : "bg-slate-100 text-slate-400")}>
<Wallet size={24} />
</div>
<div>
<p className="font-black text-slate-900 uppercase text-xs">Mercado Pago</p>
<p className="text-[10px] text-slate-400 font-bold uppercase">QR, Débito, Crédito</p>
</div>
</label>
<label className={clsx(
"flex items-center gap-4 p-6 rounded-3xl border-2 cursor-pointer transition-all",
paymentMethod === 'stripe' ? "border-slate-900 bg-slate-50 shadow-lg shadow-slate-100" : "border-slate-100 bg-white hover:border-slate-200"
)}>
<input type="radio" className="hidden" checked={paymentMethod === 'stripe'} onChange={() => setPaymentMethod('stripe')} />
<div className={clsx("p-3 rounded-2xl", paymentMethod === 'stripe' ? "bg-slate-900 text-white" : "bg-slate-100 text-slate-400")}>
<CreditCard size={24} />
</div>
<div>
<p className="font-black text-slate-900 uppercase text-xs">Tarjeta Directa</p>
<p className="text-[10px] text-slate-400 font-bold uppercase">Visa o MasterCard</p>
</div>
</label>
</div>
</section>
{/* ACCIÓN PRINCIPAL */}
<div className="flex flex-col md:flex-row gap-4 pt-6">
<button
onClick={() => setStep(5)}
className="flex-1 py-5 bg-slate-100 text-slate-500 font-black uppercase text-xs tracking-widest rounded-2xl hover:bg-slate-200 transition-all flex items-center justify-center gap-3"
disabled={isSubmitting}
>
<ArrowLeft size={16} /> Volver
</button>
<button
onClick={handlePublish}
disabled={isSubmitting}
className="flex-[2] py-5 bg-blue-600 text-white font-black uppercase text-xs tracking-[0.2em] rounded-2xl shadow-xl shadow-blue-500/20 hover:bg-blue-700 disabled:opacity-50 transition-all flex items-center justify-center gap-3 group"
>
<span className="text-white">
{isSubmitting ? 'Procesando...' : 'Pagar y Publicar Aviso'}
</span>
<ChevronRight size={18} className="text-blue-300 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
</StepWrapper>
);
}

View File

@@ -0,0 +1,233 @@
import { useState, useEffect } from 'react';
import { useWizardStore } from '../../store/wizardStore';
import { StepWrapper } from '../../components/StepWrapper';
import { useDebounce } from '../../hooks/useDebounce';
import { Eye, FileText, Calendar, ChevronRight, ArrowLeft } from 'lucide-react';
import clsx from 'clsx';
import api from '../../services/api';
interface PricingResult {
totalPrice: number;
baseCost: number;
extraCost: number;
wordCount: number;
specialCharCount: number;
details: string;
}
export default function TextEditorStep() {
const { selectedCategory, attributes, setAttribute, setStep } = useWizardStore();
const [text, setText] = useState(attributes['description'] || '');
const [days, setDays] = useState(parseInt(attributes['days'] as string) || 3);
const [pricing, setPricing] = useState<PricingResult>({
totalPrice: 0,
baseCost: 0,
extraCost: 0,
wordCount: 0,
specialCharCount: 0,
details: ''
});
const [loadingPrice, setLoadingPrice] = useState(false);
const debouncedText = useDebounce(text, 800);
useEffect(() => {
if (!selectedCategory || debouncedText.length === 0) {
setPricing({ totalPrice: 0, baseCost: 0, extraCost: 0, wordCount: 0, specialCharCount: 0, details: '' });
return;
}
const calculatePrice = async () => {
setLoadingPrice(true);
try {
const response = await api.post('/pricing/calculate', {
categoryId: selectedCategory.id,
text: debouncedText,
days: days,
isBold: false,
isFrame: false,
startDate: new Date().toISOString()
});
setPricing(response.data);
} catch (error) {
console.error('Error al calcular precio:', error);
} finally {
setLoadingPrice(false);
}
};
calculatePrice();
}, [debouncedText, days, selectedCategory]);
const handleContinue = () => {
if (text.trim().length === 0) {
alert('⚠️ Debes escribir el texto del aviso');
return;
}
setAttribute('description', text);
setAttribute('days', days.toString());
setAttribute('adFee', pricing.totalPrice.toString());
setStep(5); // Siguiente paso
};
return (
<StepWrapper>
{/* Header & Breadcrumb */}
<div className="mb-10 text-center">
<div className="flex justify-center items-center gap-2 text-[10px] font-black uppercase tracking-[0.3em] text-slate-400 mb-4">
<span>{selectedCategory?.name}</span>
<ChevronRight size={12} />
<span className="text-blue-600">Redacción</span>
</div>
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase leading-none">
Escribe tu <br />
<span className="text-blue-600">Aviso Clasificado</span>
</h2>
</div>
<div className="max-w-3xl mx-auto space-y-10">
{/* BLOQUE 1: EL EDITOR */}
<section className="bg-white p-8 rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100">
<div className="flex items-center gap-3 mb-6 text-slate-900">
<div className="p-2 bg-blue-50 rounded-xl text-blue-600">
<FileText size={20} />
</div>
<h3 className="font-black uppercase tracking-widest text-xs">Contenido del Aviso</h3>
</div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
className="w-full h-56 p-6 bg-slate-50 border-2 border-slate-100 rounded-[2rem] outline-none focus:border-blue-500 focus:bg-white transition-all font-mono text-lg leading-relaxed text-slate-700 placeholder:text-slate-300"
placeholder="Comienza a escribir aquí..."
/>
<div className="flex justify-between items-center mt-6 px-2">
<div className="flex gap-6">
<div className="flex flex-col">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Palabras</span>
<span className={clsx("text-xl font-black", pricing.wordCount > 0 ? "text-blue-600" : "text-slate-300")}>
{pricing.wordCount.toString().padStart(2, '0')}
</span>
</div>
{pricing.specialCharCount > 0 && (
<div className="flex flex-col">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Signos</span>
<span className="text-xl font-black text-orange-500">
{pricing.specialCharCount.toString().padStart(2, '0')}
</span>
</div>
)}
</div>
{loadingPrice && (
<div className="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase animate-pulse">
Sincronizando tarifa...
</div>
)}
</div>
</section>
{/* BLOQUE 2: CONFIGURACIÓN DE DÍAS */}
<section className="bg-white p-8 rounded-[2.5rem] shadow-xl border border-slate-100 flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-slate-900 rounded-2xl text-white">
<Calendar size={20} />
</div>
<div>
<h4 className="font-black uppercase text-xs text-slate-900">Duración de la oferta</h4>
<p className="text-slate-400 text-[10px] font-bold uppercase tracking-wider">¿Cuántos días se publicará?</p>
</div>
</div>
<div className="flex items-center bg-slate-100 rounded-2xl p-1 w-full md:w-48">
<button onClick={() => setDays(Math.max(1, days - 1))} className="flex-1 py-3 font-black text-xl text-slate-400 hover:text-slate-900">-</button>
<input
type="number"
value={days}
onChange={(e) => setDays(parseInt(e.target.value) || 1)}
className="w-16 text-center bg-transparent border-none outline-none font-black text-xl text-blue-600"
/>
<button onClick={() => setDays(days + 1)} className="flex-1 py-3 font-black text-xl text-slate-400 hover:text-slate-900">+</button>
</div>
</section>
{/* BLOQUE 3: PREVISUALIZACIÓN */}
<section className="bg-[#fffef5] p-8 rounded-[2.5rem] border-2 border-yellow-100 shadow-inner relative overflow-hidden group">
<div className="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity">
<Eye size={120} />
</div>
<div className="flex items-center gap-3 mb-6 text-yellow-700 relative z-10">
<Eye size={18} />
<h3 className="font-black uppercase tracking-[0.2em] text-[10px]">Vista Previa Real</h3>
</div>
<div className="bg-white p-8 rounded-[1.8rem] border border-yellow-200 shadow-sm min-h-[10rem] relative z-10">
{text ? (
<p className="text-slate-800 text-lg font-medium leading-relaxed italic uppercase">
{text}
</p>
) : (
<p className="text-slate-300 font-bold text-center py-10 uppercase tracking-widest text-xs">
El texto aparecerá aquí...
</p>
)}
</div>
</section>
{/* BLOQUE 4: COSTO FINAL */}
<section className="bg-slate-900 rounded-[3rem] p-10 text-white shadow-2xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-600/10 rounded-full blur-[80px] -mr-32 -mt-32"></div>
<div className="flex flex-col md:flex-row justify-between items-center gap-8 relative z-10">
<div className="text-center md:text-left">
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-400 mb-2 block">Total a Pagar</span>
<div className="flex items-baseline justify-center md:justify-start gap-2">
<span className="text-2xl font-black text-blue-500">$</span>
<span className="text-6xl font-black tracking-tighter font-mono">
{pricing.totalPrice.toLocaleString('es-AR', { minimumFractionDigits: 2 })}
</span>
</div>
</div>
<div className="w-full md:w-64 space-y-3 bg-white/5 p-6 rounded-3xl border border-white/10 backdrop-blur-sm">
<div className="flex justify-between text-[10px] font-black uppercase">
<span className="text-slate-500">Tarifa base</span>
<span className="text-white">${pricing.baseCost.toLocaleString()}</span>
</div>
{pricing.extraCost > 0 && (
<div className="flex justify-between text-[10px] font-black uppercase text-orange-400">
<span>Recargo texto</span>
<span>+${pricing.extraCost.toLocaleString()}</span>
</div>
)}
<div className="h-px bg-white/10 my-2"></div>
<p className="text-[9px] text-slate-500 font-bold leading-tight italic">
* {pricing.details || "Calculando tasas e impuestos..."}
</p>
</div>
</div>
</section>
{/* BOTONES DE NAVEGACIÓN */}
<div className="flex flex-col md:flex-row gap-4 pt-6">
<button
onClick={() => setStep(3)}
className="flex-1 py-5 bg-slate-100 text-slate-500 font-black uppercase text-xs tracking-widest rounded-2xl hover:bg-slate-200 transition-all flex items-center justify-center gap-3"
>
<ArrowLeft size={16} /> Volver
</button>
<button
onClick={handleContinue}
disabled={text.trim().length === 0 || loadingPrice}
className="flex-[2] py-5 bg-blue-600 text-white font-black uppercase text-xs tracking-[0.2em] rounded-2xl shadow-xl shadow-blue-500/20 hover:bg-blue-700 disabled:opacity-50 transition-all flex items-center justify-center gap-3 group"
>
Continuar a Fotos
<ChevronRight size={18} className="group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
</StepWrapper>
);
}