affine encore + la page de vote pour une meilleure UX sur mobile en particulier

This commit is contained in:
Yannick Le Duc
2025-09-16 17:32:49 +02:00
parent bfda5d3015
commit f9bb1caf32

View File

@@ -348,20 +348,22 @@ export default function PublicVotePage() {
<div className="min-h-screen bg-gray-50 vote-page">
{/* Header fixe avec le total et le bouton de validation */}
<div className="sticky top-0 z-40 bg-white shadow-sm border-b border-gray-200">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-3 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div>
<h1 className="text-lg font-semibold text-gray-900">{campaign?.title}</h1>
<p className="text-lg font-bold text-indigo-600">
{participant?.first_name} {participant?.last_name}
</p>
<div className="flex items-center space-x-2 sm:space-x-4 min-w-0 flex-1">
<div className="min-w-0 flex-1">
<h1 className="text-sm sm:text-lg font-bold text-indigo-600">
{participant?.first_name}
</h1>
<h2 className="text-sm sm:text-lg font-bold text-indigo-600">
{participant?.last_name}
</h2>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 sm:space-x-4 flex-shrink-0">
<div className="text-right">
<div className={`text-2xl font-bold transition-all duration-300 ${
<div className={`text-lg sm:text-2xl font-bold transition-all duration-300 ${
isOverBudget
? 'text-red-600 animate-pulse'
: totalVoted === campaign?.budget_per_user
@@ -372,25 +374,31 @@ export default function PublicVotePage() {
} ${isOverBudget ? 'animate-bounce' : ''}`}>
{totalVoted}€ / {campaign?.budget_per_user}€
</div>
<div className={`text-sm font-medium transition-colors duration-300 ${
<div className={`text-xs sm:text-sm font-medium transition-colors duration-300 leading-tight ${
voteStatus.status === 'success' ? 'text-green-600' :
voteStatus.status === 'warning' ? 'text-yellow-600' :
'text-red-600'
}`}>
{voteStatus.message}
{voteStatus.message.split(' ').map((word, index, array) => (
<span key={index}>
{word}
{index < array.length - 1 && index === Math.floor(array.length / 2) - 1 ? <br /> : ' '}
</span>
))}
</div>
</div>
<button
onClick={handleSubmit}
disabled={saving || totalVoted !== campaign?.budget_per_user}
className={`px-6 py-3 text-sm font-medium rounded-lg transition-all duration-200 ${
className={`px-3 sm:px-6 py-2 sm:py-3 text-xs sm:text-sm font-medium rounded-lg transition-all duration-200 ${
totalVoted === campaign?.budget_per_user
? 'bg-green-600 text-white hover:bg-green-700 shadow-lg'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
{saving ? 'Enregistrement...' : 'Valider mon vote'}
<span className="hidden sm:inline">{saving ? 'Enregistrement...' : 'Valider mon vote'}</span>
<span className="sm:hidden">{saving ? '...' : 'Valider'}</span>
</button>
</div>
</div>
@@ -402,6 +410,7 @@ export default function PublicVotePage() {
<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>
<h2 className="text-xl font-bold text-gray-900 mb-4">{campaign?.title}</h2>
<MarkdownContent
content={campaign?.description || ''}
className="mt-1 text-base font-medium text-gray-900"
@@ -472,6 +481,46 @@ export default function PublicVotePage() {
<div className="space-y-4">
{/* Slider */}
<div className="relative">
<style jsx>{`
.slider {
-webkit-appearance: none;
appearance: none;
background: #e5e7eb;
height: 8px;
border-radius: 4px;
cursor: pointer;
outline: none;
}
.slider::-webkit-slider-track {
background: #e5e7eb;
height: 8px;
border-radius: 4px;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background: #4f46e5;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
margin-top: -6px;
}
.slider::-moz-range-track {
background: #e5e7eb;
height: 8px;
border-radius: 4px;
border: none;
}
.slider::-moz-range-thumb {
background: #4f46e5;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
border: none;
}
`}</style>
<input
type="range"
min="0"
@@ -483,42 +532,79 @@ export default function PublicVotePage() {
const amount = index === 0 ? 0 : spendingTiers[index - 1];
handleVoteChange(proposition.id, amount);
}}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
className="w-full h-2 slider"
/>
{/* Marqueurs des paliers */}
<div className="relative mt-3 mb-16" style={{ marginLeft: '12px', marginRight: '24px' }}>
{/* Marqueur 0€ */}
<div className="absolute text-center" style={{ left: '0%', transform: 'translateX(-12px)' }}>
<div className="w-3 h-3 bg-gray-400 rounded-full mx-auto mb-2"></div>
<span className="text-xs text-gray-600 font-medium whitespace-nowrap">0</span>
</div>
{/* Marqueurs des paliers */}
{spendingTiers.map((tier, index) => {
// Calcul correct de la position pour correspondre au slider
const position = ((index + 1) / spendingTiers.length) * 100;
<div className="relative mt-3 mb-16" style={{ marginLeft: '6px', marginRight: '6px' }}>
{/* Fonction pour formater les montants */}
{(() => {
const formatAmount = (amount: number, isMobile: boolean) => {
if (!isMobile) return `${amount}`;
// Formatage court sur mobile pour les montants longs
if (amount >= 1000) {
if (amount % 1000 === 0) {
return `${amount / 1000}k€`;
} else {
return `${(amount / 1000).toFixed(1)}k€`;
}
}
return `${amount}`;
};
const isMobile = typeof window !== 'undefined' && window.innerWidth < 640;
return (
<div
key={`tier-${index}-${tier}`}
className="absolute text-center"
style={{
left: `${position}%`,
transform: 'translateX(-12px)'
}}
>
<div className="w-3 h-3 bg-indigo-500 rounded-full mx-auto mb-2"></div>
<span className="text-xs text-gray-600 font-medium whitespace-nowrap">{tier}</span>
</div>
<>
{/* Marqueur 0€ */}
<div
className="absolute text-center cursor-pointer hover:scale-110 transition-transform"
style={{ left: '0%', transform: 'translateX(-6px)' }}
onClick={() => handleVoteChange(proposition.id, 0)}
>
<div className="w-3 h-3 bg-gray-400 rounded-full mx-auto mb-2 hover:bg-gray-500 transition-colors"></div>
<span className="text-xs text-gray-600 font-medium whitespace-nowrap hover:text-gray-800 transition-colors">
{formatAmount(0, isMobile)}
</span>
</div>
{/* Marqueurs des paliers */}
{spendingTiers.map((tier, index) => {
// Position uniforme : espacement égal entre tous les marqueurs
// Le dernier palier doit être à 100%
const position = ((index + 1) / spendingTiers.length) * 100;
return (
<div
key={`tier-${index}-${tier}`}
className="absolute text-center cursor-pointer hover:scale-110 transition-transform"
style={{
left: `${position}%`,
transform: 'translateX(-6px)'
}}
onClick={() => handleVoteChange(proposition.id, tier)}
>
<div className="w-3 h-3 bg-indigo-500 rounded-full mx-auto mb-2 hover:bg-indigo-600 transition-colors"></div>
<span className="text-xs text-gray-600 font-medium whitespace-nowrap hover:text-gray-800 transition-colors">
{formatAmount(tier, isMobile)}
</span>
</div>
);
})}
</>
);
})}
})()}
</div>
</div>
{/* Valeur sélectionnée */}
{(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">
<span
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 cursor-pointer hover:bg-indigo-200 transition-colors"
onClick={() => handleVoteChange(proposition.id, 0)}
title="Cliquer pour remettre à 0€"
>
Vote sélectionné : {localVotes[proposition.id]}
</span>
</div>