ajout de slider sur la page de vote

This commit is contained in:
Yannick Le Duc
2025-08-25 15:32:15 +02:00
parent 06bfe11dcc
commit 4e8b592feb
3 changed files with 175 additions and 104 deletions

View File

@@ -214,9 +214,6 @@ export default function AdminPage() {
</div> </div>
</div> </div>
<div className="text-sm text-gray-500">
<span className="font-medium">Paliers de dépenses:</span> {campaign.spending_tiers}
</div>
{campaign.status === 'deposit' && ( {campaign.status === 'deposit' && (
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200"> <div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">

View File

@@ -21,6 +21,9 @@ export default function PublicVotePage() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
// Votes temporaires stockés localement
const [localVotes, setLocalVotes] = useState<Record<string, number>>({});
const [totalVoted, setTotalVoted] = useState(0); const [totalVoted, setTotalVoted] = useState(0);
useEffect(() => { useEffect(() => {
@@ -29,6 +32,12 @@ export default function PublicVotePage() {
} }
}, [campaignId, participantId]); }, [campaignId, participantId]);
// Calculer le total voté à partir des votes locaux
useEffect(() => {
const total = Object.values(localVotes).reduce((sum, amount) => sum + amount, 0);
setTotalVoted(total);
}, [localVotes]);
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -70,9 +79,12 @@ export default function PublicVotePage() {
setPropositions(propositionsWithVotes); setPropositions(propositionsWithVotes);
// Calculer le total voté // Initialiser les votes locaux avec les votes existants
const total = votes.reduce((sum, vote) => sum + vote.amount, 0); const initialVotes: Record<string, number> = {};
setTotalVoted(total); votes.forEach(vote => {
initialVotes[vote.proposition_id] = vote.amount;
});
setLocalVotes(initialVotes);
} catch (error) { } catch (error) {
console.error('Erreur lors du chargement des données:', error); console.error('Erreur lors du chargement des données:', error);
@@ -82,37 +94,18 @@ export default function PublicVotePage() {
} }
}; };
const handleVoteChange = async (propositionId: string, amount: number) => { const handleVoteChange = (propositionId: string, amount: number) => {
try { if (amount === 0) {
const existingVote = propositions.find(p => p.id === propositionId)?.vote; // Si on sélectionne "Aucun vote", on supprime le vote local
const newLocalVotes = { ...localVotes };
if (amount === 0) { delete newLocalVotes[propositionId];
// Si on sélectionne "Aucun vote", on supprime le vote existant s'il y en a un setLocalVotes(newLocalVotes);
if (existingVote) { } else {
await voteService.delete(existingVote.id); // Sinon on met à jour le vote local
} setLocalVotes(prev => ({
} else { ...prev,
// Sinon on crée ou met à jour le vote [propositionId]: amount
if (existingVote) { }));
// Mettre à jour le vote existant
await voteService.update(existingVote.id, { amount });
} else {
// Créer un nouveau vote
await voteService.create({
campaign_id: campaignId,
participant_id: participantId,
proposition_id: propositionId,
amount
});
}
}
// Recharger les données
await loadData();
} catch (error) {
console.error('Erreur lors du vote:', error);
setError('Erreur lors de l\'enregistrement du vote');
} }
}; };
@@ -126,7 +119,24 @@ export default function PublicVotePage() {
setError(''); setError('');
try { try {
// Les votes sont déjà sauvegardés, on affiche juste le succès // Supprimer tous les votes existants pour ce participant
const existingVotes = await voteService.getByParticipant(campaignId, participantId);
for (const vote of existingVotes) {
await voteService.delete(vote.id);
}
// Créer les nouveaux votes
for (const [propositionId, amount] of Object.entries(localVotes)) {
if (amount > 0) {
await voteService.create({
campaign_id: campaignId,
participant_id: participantId,
proposition_id: propositionId,
amount
});
}
}
setSuccess(true); setSuccess(true);
} catch (error) { } catch (error) {
console.error('Erreur lors de la validation:', error); console.error('Erreur lors de la validation:', error);
@@ -217,7 +227,7 @@ export default function PublicVotePage() {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Header fixe avec le total */} {/* 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-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -239,17 +249,31 @@ export default function PublicVotePage() {
</div> </div>
</div> </div>
<div className="text-right"> <div className="flex items-center space-x-4">
<div className="text-2xl font-bold text-gray-900"> <div className="text-right">
{totalVoted} / {campaign?.budget_per_user} <div className="text-2xl font-bold text-gray-900">
</div> {totalVoted} / {campaign?.budget_per_user}
<div className={`text-sm font-medium ${ </div>
voteStatus.status === 'success' ? 'text-green-600' : <div className={`text-sm font-medium ${
voteStatus.status === 'warning' ? 'text-yellow-600' : voteStatus.status === 'success' ? 'text-green-600' :
'text-red-600' voteStatus.status === 'warning' ? 'text-yellow-600' :
}`}> 'text-red-600'
{voteStatus.message} }`}>
{voteStatus.message}
</div>
</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 ${
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'}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -258,20 +282,11 @@ export default function PublicVotePage() {
<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">
{/* Informations de la campagne */} {/* Informations de la campagne */}
<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">
<h2 className="text-lg font-medium text-gray-900 mb-4">Informations sur la campagne</h2> <div className="grid grid-cols-1 md:grid-cols-1 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<h3 className="text-sm font-medium text-gray-500">Description</h3> <h3 className="text-sm font-medium text-gray-500">Description</h3>
<p className="mt-1 text-sm text-gray-900">{campaign?.description}</p> <p className="mt-1 text-sm text-gray-900">{campaign?.description}</p>
</div> </div>
<div>
<h3 className="text-sm font-medium text-gray-500">Budget par participant</h3>
<p className="mt-1 text-sm text-gray-900">{campaign?.budget_per_user}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Paliers de dépenses</h3>
<p className="mt-1 text-sm text-gray-900">{campaign?.spending_tiers}</p>
</div>
</div> </div>
</div> </div>
@@ -297,40 +312,51 @@ export default function PublicVotePage() {
<p className="text-sm text-gray-600 mb-4"> <p className="text-sm text-gray-600 mb-4">
{proposition.description} {proposition.description}
</p> </p>
<div className="text-xs text-gray-500">
<span className="font-medium">Auteur :</span> {proposition.author_first_name} {proposition.author_last_name}
</div>
</div> </div>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<label className="block text-sm font-medium text-gray-700 mb-3"> <label className="block text-sm font-medium text-gray-700 mb-3">
Votre vote pour cette proposition Pour cette proposition, vous investissez :
</label> </label>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3"> <div className="space-y-4">
{spendingTiers.map((tier) => ( {/* Slider */}
<button <div className="relative">
key={tier} <input
onClick={() => handleVoteChange(proposition.id, tier)} type="range"
className={`px-4 py-3 text-sm font-medium rounded-lg border-2 transition-all duration-200 ${ min="0"
proposition.vote?.amount === tier max={spendingTiers.length}
? 'border-indigo-500 bg-indigo-50 text-indigo-700' step="1"
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50' value={localVotes[proposition.id] ? spendingTiers.indexOf(localVotes[proposition.id]) + 1 : 0}
}`} onChange={(e) => {
> const index = parseInt(e.target.value);
{tier}€ const amount = index === 0 ? 0 : spendingTiers[index - 1];
</button> handleVoteChange(proposition.id, amount);
))} }}
<button className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
onClick={() => handleVoteChange(proposition.id, 0)} />
className={`px-4 py-3 text-sm font-medium rounded-lg border-2 transition-all duration-200 ${
!proposition.vote {/* Marqueurs des paliers */}
? 'border-gray-400 bg-gray-100 text-gray-600' <div className="flex justify-between mt-3 px-2">
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50' <div className="text-center">
}`} <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">0</span>
Aucun vote </div>
</button> {spendingTiers.map((tier, index) => (
<div key={tier} className="text-center">
<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">{tier}</span>
</div>
))}
</div>
</div>
{/* Valeur sélectionnée */}
<div className="text-center">
<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] || 0}
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -339,23 +365,6 @@ export default function PublicVotePage() {
</div> </div>
)} )}
{/* Bouton de validation */}
{propositions.length > 0 && (
<div className="mt-8 flex justify-center">
<button
onClick={handleSubmit}
disabled={saving || totalVoted !== campaign?.budget_per_user}
className={`px-8 py-4 text-lg 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'}
</button>
</div>
)}
{error && ( {error && (
<div className="mt-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md"> <div className="mt-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
{error} {error}

View File

@@ -24,3 +24,68 @@ body {
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
/* Styles personnalisés pour le slider */
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
border-radius: 4px;
background: #e5e7eb;
outline: none;
cursor: pointer;
}
.slider::-webkit-slider-track {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e5e7eb;
border: 1px solid #d1d5db;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #4f46e5;
cursor: pointer;
border: 2px solid #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
}
.slider::-webkit-slider-thumb:hover {
background: #3730a3;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.slider::-moz-range-track {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e5e7eb;
border: 1px solid #d1d5db;
outline: none;
}
.slider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: #4f46e5;
cursor: pointer;
border: 2px solid #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
}
.slider::-moz-range-thumb:hover {
background: #3730a3;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}