affine encore + la page de vote pour une meilleure UX sur mobile en particulier
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user