ajout de slider sur la page de vote
This commit is contained in:
@@ -214,9 +214,6 @@ export default function AdminPage() {
|
||||
</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' && (
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
|
||||
@@ -21,6 +21,9 @@ export default function PublicVotePage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Votes temporaires stockés localement
|
||||
const [localVotes, setLocalVotes] = useState<Record<string, number>>({});
|
||||
const [totalVoted, setTotalVoted] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -29,6 +32,12 @@ export default function PublicVotePage() {
|
||||
}
|
||||
}, [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 () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -70,9 +79,12 @@ export default function PublicVotePage() {
|
||||
|
||||
setPropositions(propositionsWithVotes);
|
||||
|
||||
// Calculer le total voté
|
||||
const total = votes.reduce((sum, vote) => sum + vote.amount, 0);
|
||||
setTotalVoted(total);
|
||||
// Initialiser les votes locaux avec les votes existants
|
||||
const initialVotes: Record<string, number> = {};
|
||||
votes.forEach(vote => {
|
||||
initialVotes[vote.proposition_id] = vote.amount;
|
||||
});
|
||||
setLocalVotes(initialVotes);
|
||||
|
||||
} catch (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) => {
|
||||
try {
|
||||
const existingVote = propositions.find(p => p.id === propositionId)?.vote;
|
||||
|
||||
if (amount === 0) {
|
||||
// Si on sélectionne "Aucun vote", on supprime le vote existant s'il y en a un
|
||||
if (existingVote) {
|
||||
await voteService.delete(existingVote.id);
|
||||
}
|
||||
} else {
|
||||
// Sinon on crée ou met à jour le vote
|
||||
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');
|
||||
const handleVoteChange = (propositionId: string, amount: number) => {
|
||||
if (amount === 0) {
|
||||
// Si on sélectionne "Aucun vote", on supprime le vote local
|
||||
const newLocalVotes = { ...localVotes };
|
||||
delete newLocalVotes[propositionId];
|
||||
setLocalVotes(newLocalVotes);
|
||||
} else {
|
||||
// Sinon on met à jour le vote local
|
||||
setLocalVotes(prev => ({
|
||||
...prev,
|
||||
[propositionId]: amount
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,7 +119,24 @@ export default function PublicVotePage() {
|
||||
setError('');
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la validation:', error);
|
||||
@@ -217,7 +227,7 @@ export default function PublicVotePage() {
|
||||
|
||||
return (
|
||||
<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="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -239,17 +249,31 @@ export default function PublicVotePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{totalVoted}€ / {campaign?.budget_per_user}€
|
||||
</div>
|
||||
<div className={`text-sm font-medium ${
|
||||
voteStatus.status === 'success' ? 'text-green-600' :
|
||||
voteStatus.status === 'warning' ? 'text-yellow-600' :
|
||||
'text-red-600'
|
||||
}`}>
|
||||
{voteStatus.message}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{totalVoted}€ / {campaign?.budget_per_user}€
|
||||
</div>
|
||||
<div className={`text-sm font-medium ${
|
||||
voteStatus.status === 'success' ? 'text-green-600' :
|
||||
voteStatus.status === 'warning' ? 'text-yellow-600' :
|
||||
'text-red-600'
|
||||
}`}>
|
||||
{voteStatus.message}
|
||||
</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>
|
||||
@@ -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">
|
||||
{/* Informations de la campagne */}
|
||||
<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-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Description</h3>
|
||||
<p className="mt-1 text-sm text-gray-900">{campaign?.description}</p>
|
||||
</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>
|
||||
|
||||
@@ -297,40 +312,51 @@ export default function PublicVotePage() {
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
{proposition.description}
|
||||
</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 className="mt-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Votre vote pour cette proposition
|
||||
Pour cette proposition, vous investissez :
|
||||
</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||
{spendingTiers.map((tier) => (
|
||||
<button
|
||||
key={tier}
|
||||
onClick={() => handleVoteChange(proposition.id, tier)}
|
||||
className={`px-4 py-3 text-sm font-medium rounded-lg border-2 transition-all duration-200 ${
|
||||
proposition.vote?.amount === tier
|
||||
? 'border-indigo-500 bg-indigo-50 text-indigo-700'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{tier}€
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => handleVoteChange(proposition.id, 0)}
|
||||
className={`px-4 py-3 text-sm font-medium rounded-lg border-2 transition-all duration-200 ${
|
||||
!proposition.vote
|
||||
? 'border-gray-400 bg-gray-100 text-gray-600'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Aucun vote
|
||||
</button>
|
||||
<div className="space-y-4">
|
||||
{/* Slider */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={spendingTiers.length}
|
||||
step="1"
|
||||
value={localVotes[proposition.id] ? spendingTiers.indexOf(localVotes[proposition.id]) + 1 : 0}
|
||||
onChange={(e) => {
|
||||
const index = parseInt(e.target.value);
|
||||
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"
|
||||
/>
|
||||
|
||||
{/* Marqueurs des paliers */}
|
||||
<div className="flex justify-between mt-3 px-2">
|
||||
<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>
|
||||
</div>
|
||||
{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>
|
||||
@@ -339,23 +365,6 @@ export default function PublicVotePage() {
|
||||
</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 && (
|
||||
<div className="mt-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
|
||||
{error}
|
||||
|
||||
@@ -24,3 +24,68 @@ body {
|
||||
color: var(--foreground);
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user