ajout de slider sur la page de vote
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
const existingVote = propositions.find(p => p.id === propositionId)?.vote;
|
|
||||||
|
|
||||||
if (amount === 0) {
|
if (amount === 0) {
|
||||||
// Si on sélectionne "Aucun vote", on supprime le vote existant s'il y en a un
|
// Si on sélectionne "Aucun vote", on supprime le vote local
|
||||||
if (existingVote) {
|
const newLocalVotes = { ...localVotes };
|
||||||
await voteService.delete(existingVote.id);
|
delete newLocalVotes[propositionId];
|
||||||
}
|
setLocalVotes(newLocalVotes);
|
||||||
} else {
|
} else {
|
||||||
// Sinon on crée ou met à jour le vote
|
// Sinon on met à jour le vote local
|
||||||
if (existingVote) {
|
setLocalVotes(prev => ({
|
||||||
// Mettre à jour le vote existant
|
...prev,
|
||||||
await voteService.update(existingVote.id, { amount });
|
[propositionId]: 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,6 +249,7 @@ export default function PublicVotePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-2xl font-bold text-gray-900">
|
<div className="text-2xl font-bold text-gray-900">
|
||||||
{totalVoted}€ / {campaign?.budget_per_user}€
|
{totalVoted}€ / {campaign?.budget_per_user}€
|
||||||
@@ -251,6 +262,19 @@ export default function PublicVotePage() {
|
|||||||
{voteStatus.message}
|
{voteStatus.message}
|
||||||
</div>
|
</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>
|
</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,62 +312,56 @@ 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
|
|
||||||
? '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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bouton de validation */}
|
{/* Marqueurs des paliers */}
|
||||||
{propositions.length > 0 && (
|
<div className="flex justify-between mt-3 px-2">
|
||||||
<div className="mt-8 flex justify-center">
|
<div className="text-center">
|
||||||
<button
|
<div className="w-3 h-3 bg-gray-400 rounded-full mx-auto mb-2"></div>
|
||||||
onClick={handleSubmit}
|
<span className="text-xs text-gray-600 font-medium">0€</span>
|
||||||
disabled={saving || totalVoted !== campaign?.budget_per_user}
|
</div>
|
||||||
className={`px-8 py-4 text-lg font-medium rounded-lg transition-all duration-200 ${
|
{spendingTiers.map((tier, index) => (
|
||||||
totalVoted === campaign?.budget_per_user
|
<div key={tier} className="text-center">
|
||||||
? 'bg-green-600 text-white hover:bg-green-700 shadow-lg'
|
<div className="w-3 h-3 bg-indigo-500 rounded-full mx-auto mb-2"></div>
|
||||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
<span className="text-xs text-gray-600 font-medium">{tier}€</span>
|
||||||
}`}
|
</div>
|
||||||
>
|
))}
|
||||||
{saving ? 'Enregistrement...' : 'Valider mon vote'}
|
</div>
|
||||||
</button>
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user