Improve public pages
This commit is contained in:
435
frontend/src/pages/public/FeedbackPage.tsx
Normal file
435
frontend/src/pages/public/FeedbackPage.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PublicLayout } from '../../components/layout';
|
||||
import {
|
||||
MessageSquare,
|
||||
Heart,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Lightbulb,
|
||||
AlertTriangle,
|
||||
Send,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Star
|
||||
} from 'lucide-react';
|
||||
|
||||
interface FeedbackCategory {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const FeedbackPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [formState, setFormState] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
category: 'suggestion' as 'suggestion' | 'bug' | 'feature' | 'praise' | 'complaint',
|
||||
title: '',
|
||||
description: '',
|
||||
rating: 0,
|
||||
});
|
||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
const categories: FeedbackCategory[] = [
|
||||
{
|
||||
id: 'suggestion',
|
||||
title: 'Sugerencia',
|
||||
description: 'Ideas para mejorar el producto',
|
||||
icon: Lightbulb,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'feature',
|
||||
title: 'Nueva Funcionalidad',
|
||||
description: 'Solicita una característica nueva',
|
||||
icon: Star,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
id: 'bug',
|
||||
title: 'Reportar Bug',
|
||||
description: 'Algo no funciona como debería',
|
||||
icon: AlertTriangle,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
id: 'praise',
|
||||
title: 'Elogio',
|
||||
description: '¡Algo te encanta!',
|
||||
icon: Heart,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'complaint',
|
||||
title: 'Queja',
|
||||
description: 'Algo te molesta o decepciona',
|
||||
icon: ThumbsDown,
|
||||
color: 'amber',
|
||||
},
|
||||
];
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
setFormState({
|
||||
...formState,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRatingChange = (rating: number) => {
|
||||
setFormState({
|
||||
...formState,
|
||||
rating,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitStatus('loading');
|
||||
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// In production, this would be an actual API call
|
||||
console.log('Feedback submitted:', formState);
|
||||
|
||||
setSubmitStatus('success');
|
||||
setTimeout(() => {
|
||||
setSubmitStatus('idle');
|
||||
setFormState({
|
||||
name: '',
|
||||
email: '',
|
||||
category: 'suggestion',
|
||||
title: '',
|
||||
description: '',
|
||||
rating: 0,
|
||||
});
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const getCategoryColor = (color: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
blue: 'from-blue-500/10 to-blue-600/10 border-blue-500/20',
|
||||
purple: 'from-purple-500/10 to-purple-600/10 border-purple-500/20',
|
||||
red: 'from-red-500/10 to-red-600/10 border-red-500/20',
|
||||
green: 'from-green-500/10 to-green-600/10 border-green-500/20',
|
||||
amber: 'from-amber-500/10 to-amber-600/10 border-amber-500/20',
|
||||
};
|
||||
return colors[color] || colors.blue;
|
||||
};
|
||||
|
||||
const getCategoryIconColor = (color: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
blue: 'text-blue-600',
|
||||
purple: 'text-purple-600',
|
||||
red: 'text-red-600',
|
||||
green: 'text-green-600',
|
||||
amber: 'text-amber-600',
|
||||
};
|
||||
return colors[color] || colors.blue;
|
||||
};
|
||||
|
||||
return (
|
||||
<PublicLayout
|
||||
variant="default"
|
||||
contentPadding="default"
|
||||
headerProps={{
|
||||
showThemeToggle: true,
|
||||
showAuthButtons: true,
|
||||
showLanguageSelector: true,
|
||||
variant: 'default',
|
||||
}}
|
||||
>
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-br from-[var(--bg-primary)] via-[var(--bg-secondary)] to-[var(--color-primary)]/5 py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-4xl mx-auto">
|
||||
<div className="inline-flex items-center gap-2 bg-[var(--color-primary)]/10 text-[var(--color-primary)] px-4 py-2 rounded-full text-sm font-medium mb-6">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>Tu Opinión Importa</span>
|
||||
</div>
|
||||
<h1 className="text-4xl lg:text-6xl font-extrabold text-[var(--text-primary)] mb-6">
|
||||
Ayúdanos a Mejorar
|
||||
<span className="block text-[var(--color-primary)]">Panadería IA</span>
|
||||
</h1>
|
||||
<p className="text-xl text-[var(--text-secondary)] leading-relaxed mb-8">
|
||||
Tu feedback es fundamental para construir el mejor producto para panaderías artesanales
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why Feedback Matters */}
|
||||
<section className="py-20 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
|
||||
¿Por Qué Tu Feedback Es Importante?
|
||||
</h2>
|
||||
<p className="text-xl text-[var(--text-secondary)]">
|
||||
Estamos construyendo esto juntos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-16">
|
||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] text-center">
|
||||
<ThumbsUp className="w-12 h-12 text-[var(--color-primary)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||
Priorizamos Tu Feedback
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Cada sugerencia es revisada y considerada para el roadmap de producto
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] text-center">
|
||||
<Lightbulb className="w-12 h-12 text-[var(--color-primary)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||
Tus Ideas Nos Inspiran
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Las mejores funcionalidades vienen directamente de nuestros usuarios
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border-primary)] text-center">
|
||||
<Heart className="w-12 h-12 text-[var(--color-primary)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-2">
|
||||
Comunidad Activa
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Contribuyes a crear una herramienta que toda la comunidad panadera disfrutará
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feedback Form */}
|
||||
<section className="py-20 bg-[var(--bg-secondary)]">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] mb-4">
|
||||
Comparte Tu Feedback
|
||||
</h2>
|
||||
<p className="text-xl text-[var(--text-secondary)]">
|
||||
Queremos escucharte
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Category Selection */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold text-[var(--text-primary)] mb-4 text-center">
|
||||
¿Qué tipo de feedback quieres compartir?
|
||||
</h3>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{categories.map((category) => {
|
||||
const CategoryIcon = category.icon;
|
||||
const isSelected = formState.category === category.id;
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
type="button"
|
||||
onClick={() => setFormState({ ...formState, category: category.id as any })}
|
||||
className={`bg-gradient-to-br ${getCategoryColor(category.color)} rounded-xl p-6 border-2 transition-all text-left ${
|
||||
isSelected
|
||||
? 'border-[var(--color-primary)] shadow-lg'
|
||||
: 'border-transparent hover:border-[var(--color-primary)]/30'
|
||||
}`}
|
||||
>
|
||||
<CategoryIcon className={`w-8 h-8 ${getCategoryIconColor(category.color)} mb-3`} />
|
||||
<h4 className="font-bold text-[var(--text-primary)] mb-1">{category.title}</h4>
|
||||
<p className="text-xs text-[var(--text-secondary)]">{category.description}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="bg-[var(--bg-primary)] rounded-2xl p-8 border border-[var(--border-primary)]">
|
||||
{/* Success/Error Messages */}
|
||||
{submitStatus === 'success' && (
|
||||
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>¡Gracias por tu feedback!</strong> Lo revisaremos pronto.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{submitStatus === 'error' && (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||
<p className="text-sm text-red-800 dark:text-red-200">
|
||||
<strong>Error al enviar.</strong> Por favor, inténtalo de nuevo.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Tu Nombre <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formState.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formState.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||
placeholder="tu@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating */}
|
||||
{formState.category === 'praise' && (
|
||||
<div className="mt-6">
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-3">
|
||||
¿Cómo calificarías tu experiencia? <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{[1, 2, 3, 4, 5].map((rating) => (
|
||||
<button
|
||||
key={rating}
|
||||
type="button"
|
||||
onClick={() => handleRatingChange(rating)}
|
||||
className="transition-transform hover:scale-110"
|
||||
>
|
||||
<Star
|
||||
className={`w-10 h-10 ${
|
||||
rating <= formState.rating
|
||||
? 'fill-yellow-500 text-yellow-500'
|
||||
: 'text-[var(--border-primary)]'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<div className="mt-6">
|
||||
<label htmlFor="title" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Título <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formState.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors"
|
||||
placeholder="Resumen en una línea"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mt-6">
|
||||
<label htmlFor="description" className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Descripción Detallada <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formState.description}
|
||||
onChange={handleChange}
|
||||
required
|
||||
rows={6}
|
||||
className="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] focus:outline-none focus:border-[var(--color-primary)] transition-colors resize-none"
|
||||
placeholder={
|
||||
formState.category === 'bug'
|
||||
? 'Describe el problema: ¿qué esperabas que pasara? ¿qué pasó en su lugar? ¿cómo podemos reproducirlo?'
|
||||
: formState.category === 'feature'
|
||||
? 'Describe la funcionalidad que te gustaría ver: ¿qué problema resolvería? ¿cómo lo imaginas?'
|
||||
: 'Cuéntanos más sobre tu feedback...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="mt-8">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitStatus === 'loading'}
|
||||
className="w-full flex items-center justify-center gap-2 px-8 py-4 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:shadow-xl transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
>
|
||||
{submitStatus === 'loading' ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
<span>Enviando...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-5 h-5" />
|
||||
<span>Enviar Feedback</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-[var(--text-tertiary)] text-center mt-4">
|
||||
Tu feedback nos ayuda a mejorar. ¡Gracias por tomarte el tiempo!
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Other Ways to Connect */}
|
||||
<section className="py-20 bg-[var(--bg-primary)]">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="bg-gradient-to-br from-[var(--color-primary)]/10 to-orange-600/10 rounded-2xl p-8 border border-[var(--color-primary)]/20 text-center">
|
||||
<h3 className="text-2xl font-bold text-[var(--text-primary)] mb-4">
|
||||
¿Prefieres Hablar Directamente?
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-6">
|
||||
Estamos disponibles por múltiples canales
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<a
|
||||
href="/help/support"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-[var(--color-primary)] text-white rounded-xl font-bold hover:shadow-xl transition-all hover:scale-105"
|
||||
>
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span>Contactar Soporte</span>
|
||||
</a>
|
||||
<a
|
||||
href="/help"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 border-2 border-[var(--border-primary)] text-[var(--text-primary)] rounded-xl font-medium hover:border-[var(--color-primary)] transition-all"
|
||||
>
|
||||
Centro de Ayuda
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackPage;
|
||||
Reference in New Issue
Block a user