fine tux à max la page de vote (better ux)
This commit is contained in:
@@ -27,6 +27,9 @@ export default function PublicVotePage() {
|
||||
const [localVotes, setLocalVotes] = useState<Record<string, number>>({});
|
||||
const [totalVoted, setTotalVoted] = useState(0);
|
||||
const [isRandomOrder, setIsRandomOrder] = useState(false);
|
||||
const [isCompactView, setIsCompactView] = useState(false);
|
||||
const [currentVisibleProposition, setCurrentVisibleProposition] = useState(1);
|
||||
const [isOverBudget, setIsOverBudget] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (campaignId && participantId) {
|
||||
@@ -38,7 +41,52 @@ export default function PublicVotePage() {
|
||||
useEffect(() => {
|
||||
const total = Object.values(localVotes).reduce((sum, amount) => sum + amount, 0);
|
||||
setTotalVoted(total);
|
||||
}, [localVotes]);
|
||||
|
||||
// Vérifier si on dépasse le budget
|
||||
if (campaign && total > campaign.budget_per_user) {
|
||||
setIsOverBudget(true);
|
||||
// Arrêter la vibration après 1 seconde
|
||||
setTimeout(() => setIsOverBudget(false), 1000);
|
||||
} else {
|
||||
setIsOverBudget(false);
|
||||
}
|
||||
}, [localVotes, campaign]);
|
||||
|
||||
// Observer les propositions visibles
|
||||
useEffect(() => {
|
||||
if (propositions.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
let highestVisibleIndex = 1;
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const propositionIndex = parseInt(entry.target.getAttribute('data-proposition-index') || '1');
|
||||
if (propositionIndex > highestVisibleIndex) {
|
||||
highestVisibleIndex = propositionIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (highestVisibleIndex > 1) {
|
||||
setCurrentVisibleProposition(highestVisibleIndex);
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: 0.3, // La proposition doit être visible à 30% pour être considérée comme active
|
||||
rootMargin: '-10% 0px -10% 0px' // Zone de détection réduite
|
||||
}
|
||||
);
|
||||
|
||||
// Attendre que le DOM soit mis à jour
|
||||
setTimeout(() => {
|
||||
const propositionElements = document.querySelectorAll('[data-proposition-index]');
|
||||
propositionElements.forEach((element) => observer.observe(element));
|
||||
}, 100);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [propositions, isCompactView]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
@@ -253,10 +301,18 @@ export default function PublicVotePage() {
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
<div className={`text-2xl font-bold transition-all duration-300 ${
|
||||
isOverBudget
|
||||
? 'text-red-600 animate-pulse'
|
||||
: totalVoted === campaign?.budget_per_user
|
||||
? 'text-green-600 scale-105'
|
||||
: totalVoted > 0
|
||||
? 'text-indigo-600'
|
||||
: 'text-gray-900'
|
||||
} ${isOverBudget ? 'animate-bounce' : ''}`}>
|
||||
{totalVoted}€ / {campaign?.budget_per_user}€
|
||||
</div>
|
||||
<div className={`text-sm font-medium ${
|
||||
<div className={`text-sm font-medium transition-colors duration-300 ${
|
||||
voteStatus.status === 'success' ? 'text-green-600' :
|
||||
voteStatus.status === 'warning' ? 'text-yellow-600' :
|
||||
'text-red-600'
|
||||
@@ -281,18 +337,15 @@ export default function PublicVotePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 pb-20">
|
||||
{/* Informations de la campagne */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
|
||||
<div>
|
||||
<p className="mt-1 text-sm text-gray-900 whitespace-pre-wrap">{campaign?.description}</p>
|
||||
<p className="mt-1 text-base font-medium text-gray-900 whitespace-pre-wrap leading-relaxed">{campaign?.description}</p>
|
||||
{isRandomOrder && (
|
||||
<div className="mt-3 p-2 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-xs text-blue-700 flex items-center gap-1">
|
||||
<span className="text-blue-500">ℹ️</span>
|
||||
Les propositions sont affichées dans un ordre aléatoire pour éviter les biais liés à l'ordre de présentation.
|
||||
</p>
|
||||
<div className="mt-4 text-xs text-gray-500 italic">
|
||||
ℹ️ Les propositions sont affichées dans un ordre aléatoire pour éviter les biais liés à l'ordre de présentation.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -309,35 +362,42 @@ export default function PublicVotePage() {
|
||||
<p className="mt-1 text-sm text-gray-500">Aucune proposition n'a été soumise pour cette campagne.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{propositions.map((proposition) => (
|
||||
<div className={`${isCompactView ? 'space-y-3' : 'space-y-6'}`}>
|
||||
{propositions.map((proposition, index) => (
|
||||
<div
|
||||
key={proposition.id}
|
||||
data-proposition-index={index + 1}
|
||||
className={`rounded-lg shadow-sm border overflow-hidden transition-all duration-200 relative ${
|
||||
localVotes[proposition.id] && localVotes[proposition.id] > 0
|
||||
? 'border-indigo-400 shadow-lg bg-indigo-100'
|
||||
: 'bg-white border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{!isCompactView && (
|
||||
<div className="absolute -top-1 left-4 bg-white px-2 text-xs text-gray-500 font-medium z-10 border border-gray-200 rounded-t">
|
||||
Proposition
|
||||
</div>
|
||||
<div className="p-6">
|
||||
)}
|
||||
<div className={`${isCompactView ? 'p-4' : 'p-6'}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
<h3 className={`font-medium text-gray-900 ${isCompactView ? 'text-base mb-1' : 'text-lg mb-2'}`}>
|
||||
{proposition.title}
|
||||
</h3>
|
||||
{!isCompactView && (
|
||||
<p className="text-sm text-gray-600 mb-4 whitespace-pre-wrap">
|
||||
{proposition.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className={isCompactView ? 'mt-3' : 'mt-6'}>
|
||||
{!isCompactView && (
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Pour cette proposition, vous investissez :
|
||||
</label>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
{/* Slider */}
|
||||
<div className="relative">
|
||||
@@ -384,7 +444,7 @@ export default function PublicVotePage() {
|
||||
</div>
|
||||
|
||||
{/* Valeur sélectionnée */}
|
||||
{(localVotes[proposition.id] && localVotes[proposition.id] > 0) && (
|
||||
{(localVotes[proposition.id] && localVotes[proposition.id] > 0) && !isCompactView && (
|
||||
<div className="text-center mt-12">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800">
|
||||
Vote sélectionné : {localVotes[proposition.id]}€
|
||||
@@ -405,6 +465,34 @@ export default function PublicVotePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Barre fixe en bas */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-40 bg-white shadow-lg border-t border-gray-200">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
Proposition {currentVisibleProposition} / {propositions.length}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<span>Juste les titres</span>
|
||||
<button
|
||||
onClick={() => setIsCompactView(!isCompactView)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
||||
!isCompactView ? 'bg-indigo-600' : 'bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
|
||||
!isCompactView ? 'translate-x-5' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span>Avec descriptions</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user