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">
|
<div className="min-h-screen bg-gray-50 vote-page">
|
||||||
{/* Header fixe avec le total et le bouton de validation */}
|
{/* 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="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 justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-2 sm:space-x-4 min-w-0 flex-1">
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<h1 className="text-lg font-semibold text-gray-900">{campaign?.title}</h1>
|
<h1 className="text-sm sm:text-lg font-bold text-indigo-600">
|
||||||
<p className="text-lg font-bold text-indigo-600">
|
{participant?.first_name}
|
||||||
{participant?.first_name} {participant?.last_name}
|
</h1>
|
||||||
</p>
|
<h2 className="text-sm sm:text-lg font-bold text-indigo-600">
|
||||||
|
{participant?.last_name}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</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-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
|
isOverBudget
|
||||||
? 'text-red-600 animate-pulse'
|
? 'text-red-600 animate-pulse'
|
||||||
: totalVoted === campaign?.budget_per_user
|
: totalVoted === campaign?.budget_per_user
|
||||||
@@ -372,25 +374,31 @@ export default function PublicVotePage() {
|
|||||||
} ${isOverBudget ? 'animate-bounce' : ''}`}>
|
} ${isOverBudget ? 'animate-bounce' : ''}`}>
|
||||||
{totalVoted}€ / {campaign?.budget_per_user}€
|
{totalVoted}€ / {campaign?.budget_per_user}€
|
||||||
</div>
|
</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 === 'success' ? 'text-green-600' :
|
||||||
voteStatus.status === 'warning' ? 'text-yellow-600' :
|
voteStatus.status === 'warning' ? 'text-yellow-600' :
|
||||||
'text-red-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={saving || totalVoted !== campaign?.budget_per_user}
|
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
|
totalVoted === campaign?.budget_per_user
|
||||||
? 'bg-green-600 text-white hover:bg-green-700 shadow-lg'
|
? 'bg-green-600 text-white hover:bg-green-700 shadow-lg'
|
||||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
: '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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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="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 className="grid grid-cols-1 md:grid-cols-1 gap-6">
|
||||||
<div>
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{campaign?.title}</h2>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
content={campaign?.description || ''}
|
content={campaign?.description || ''}
|
||||||
className="mt-1 text-base font-medium text-gray-900"
|
className="mt-1 text-base font-medium text-gray-900"
|
||||||
@@ -472,6 +481,46 @@ export default function PublicVotePage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Slider */}
|
{/* Slider */}
|
||||||
<div className="relative">
|
<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
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -483,42 +532,79 @@ export default function PublicVotePage() {
|
|||||||
const amount = index === 0 ? 0 : spendingTiers[index - 1];
|
const amount = index === 0 ? 0 : spendingTiers[index - 1];
|
||||||
handleVoteChange(proposition.id, amount);
|
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 */}
|
{/* Marqueurs des paliers */}
|
||||||
<div className="relative mt-3 mb-16" style={{ marginLeft: '12px', marginRight: '24px' }}>
|
<div className="relative mt-3 mb-16" style={{ marginLeft: '6px', marginRight: '6px' }}>
|
||||||
{/* Marqueur 0€ */}
|
{/* Fonction pour formater les montants */}
|
||||||
<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>
|
const formatAmount = (amount: number, isMobile: boolean) => {
|
||||||
<span className="text-xs text-gray-600 font-medium whitespace-nowrap">0€</span>
|
if (!isMobile) return `${amount}€`;
|
||||||
</div>
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
{/* Marqueurs des paliers */}
|
|
||||||
{spendingTiers.map((tier, index) => {
|
|
||||||
// Calcul correct de la position pour correspondre au slider
|
|
||||||
const position = ((index + 1) / spendingTiers.length) * 100;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
key={`tier-${index}-${tier}`}
|
{/* Marqueur 0€ */}
|
||||||
className="absolute text-center"
|
<div
|
||||||
style={{
|
className="absolute text-center cursor-pointer hover:scale-110 transition-transform"
|
||||||
left: `${position}%`,
|
style={{ left: '0%', transform: 'translateX(-6px)' }}
|
||||||
transform: 'translateX(-12px)'
|
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>
|
||||||
<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 hover:text-gray-800 transition-colors">
|
||||||
<span className="text-xs text-gray-600 font-medium whitespace-nowrap">{tier}€</span>
|
{formatAmount(0, isMobile)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Valeur sélectionnée */}
|
{/* Valeur sélectionnée */}
|
||||||
{(localVotes[proposition.id] && localVotes[proposition.id] > 0) && !isCompactView && (
|
{(localVotes[proposition.id] && localVotes[proposition.id] > 0) && !isCompactView && (
|
||||||
<div className="text-center mt-12">
|
<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]}€
|
Vote sélectionné : {localVotes[proposition.id]}€
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user