Amélioration UI

This commit is contained in:
ocroguennec committed 2026-08-16 15:34:42 +02:00
1 parent f4c590b3a3
commit f76e7c9932
2 files changed
+142 -10

No files matched your search

@@ -0,0 +1,136 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { fmtEUR } from '../utils/format.js';
/**
* InvestissementSelect — combobox pour le champ "Investissement" de RembFormModal.
*
* Remplace le <select> natif : le navigateur ne permet pas de limiter la hauteur du popup
* d'un <select> natif via CSS, ce qui produisait une liste déroulante démesurément haute
* quand beaucoup d'investissements existent.
*
* Positionnement en `position: fixed` (calculé via getBoundingClientRect, comme InvSelect.jsx) —
* pas en `position: absolute` — car ce champ est utilisé dans RembFormModal dont le conteneur
* `.card` a `overflow: auto` (voir Modal.jsx) : un dropdown en absolute y serait rogné quand la
* fenêtre est basse. Le dropdown bascule aussi au-dessus du champ s'il n'y a pas assez de place
* en dessous, et se ferme au scroll/resize (repositionnement non géré en direct, comme InvSelect).
*
* Props :
* value : '' | 'BONUS_PARRAINAGE' | 'BONUS_PLATEFORME' | id (string/number)
* onChange : (value) => void
* options : investissementOptions — { id, plateforme_nom, nom_projet, montant_investi }[]
*/
export default function InvestissementSelect({ value, onChange, options = [] }) {
const [open, setOpen] = useState(false);
const [dropPos, setDropPos] = useState({ left: 0, width: 0, maxHeight: 260, direction: 'down', top: 0, bottom: 0 });
const wrapRef = useRef(null);
const triggerRef = useRef(null);
const selectedOption = options.find(i => String(i.id) === String(value));
const label = value === 'BONUS_PARRAINAGE' ? '🎁 Bonus Parrainage'
: value === 'BONUS_PLATEFORME' ? '🏆 Bonus Plateforme'
: selectedOption ? `${selectedOption.plateforme_nom} — ${selectedOption.nom_projet} (${fmtEUR(selectedOption.montant_investi)})`
: null;
useLayoutEffect(() => {
if (!open || !triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const margin = 8;
const spaceBelow = window.innerHeight - rect.bottom - margin;
const spaceAbove = rect.top - margin;
const desired = 260; // ~ moitié de la hauteur qu'affichait le <select> natif sans contrainte
if (spaceBelow >= 140 || spaceBelow >= spaceAbove) {
setDropPos({
direction: 'down', left: rect.left, width: rect.width,
top: rect.bottom + 4, bottom: 0,
maxHeight: Math.max(120, Math.min(desired, spaceBelow)),
});
} else {
setDropPos({
direction: 'up', left: rect.left, width: rect.width,
top: 0, bottom: window.innerHeight - rect.top + 4,
maxHeight: Math.max(120, Math.min(desired, spaceAbove)),
});
}
}, [open]);
useEffect(() => {
if (!open) return;
const close = (e) => {
if (wrapRef.current?.contains(e.target)) return;
const drop = document.getElementById('investissement-select-dropdown-portal');
if (drop?.contains(e.target)) return;
setOpen(false);
};
const closeOnScroll = (e) => {
const drop = document.getElementById('investissement-select-dropdown-portal');
if (drop?.contains(e.target)) return; // scroll dans le dropdown — on garde ouvert
setOpen(false);
};
document.addEventListener('mousedown', close);
window.addEventListener('scroll', closeOnScroll, true);
window.addEventListener('resize', closeOnScroll);
return () => {
document.removeEventListener('mousedown', close);
window.removeEventListener('scroll', closeOnScroll, true);
window.removeEventListener('resize', closeOnScroll);
};
}, [open]);
const select = (v) => {
onChange(v);
setOpen(false);
};
const rowStyle = (active) => ({
background: active ? 'var(--surface-2)' : undefined,
fontWeight: active ? 600 : 400,
color: active ? 'var(--primary)' : undefined,
});
return (
<div ref={wrapRef} className="cat-select-wrap">
<button
ref={triggerRef}
type="button"
className={`cat-select-trigger${open ? ' open' : ''}`}
onClick={() => setOpen(o => !o)}
aria-haspopup="listbox"
aria-expanded={open}
>
<span className="cat-select-label" style={!label ? { color: 'var(--text-muted)' } : undefined}>
{label ?? '—'}
</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
style={{ flexShrink: 0, transition: 'transform .15s', transform: open ? 'rotate(0deg)' : 'rotate(180deg)' }}
aria-hidden="true">
<path d="M18 15l-6-6-6 6"/>
</svg>
</button>
{open && (
<div
id="investissement-select-dropdown-portal"
className="cat-select-dropdown"
role="listbox"
style={{
position: 'fixed', left: dropPos.left, width: dropPos.width,
maxHeight: dropPos.maxHeight,
...(dropPos.direction === 'down' ? { top: dropPos.top } : { bottom: dropPos.bottom }),
}}
>
<button type="button" style={rowStyle(value === '')} onClick={() => select('')}>—</button>
<button type="button" style={rowStyle(value === 'BONUS_PARRAINAGE')} onClick={() => select('BONUS_PARRAINAGE')}>🎁 Bonus Parrainage</button>
<button type="button" style={rowStyle(value === 'BONUS_PLATEFORME')} onClick={() => select('BONUS_PLATEFORME')}>🏆 Bonus Plateforme</button>
<div className="cat-select-sep" />
{options.map(i => (
<button key={i.id} type="button" style={rowStyle(String(value) === String(i.id))} onClick={() => select(String(i.id))}>
{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})
</button>
))}
</div>
)}
</div>
);
}
+6 -10
View File
@@ -1,4 +1,5 @@
import Modal from './Modal.jsx'; import Modal from './Modal.jsx';
import InvestissementSelect from './InvestissementSelect.jsx';
import { fmtEUR } from '../utils/format.js'; import { fmtEUR } from '../utils/format.js';
/* ── Helpers purs (dupliqués volontairement, pas d'état) ── */ /* ── Helpers purs (dupliqués volontairement, pas d'état) ── */
@@ -128,16 +129,11 @@ export default function RembFormModal({
{!locked && ( {!locked && (
<div style={{ gridColumn: isBonus ? 'span 1' : 'span 3' }}> <div style={{ gridColumn: isBonus ? 'span 1' : 'span 3' }}>
<label>Investissement *</label> <label>Investissement *</label>
<select required value={form.investissement_id} <InvestissementSelect
onChange={e => onFieldChange('investissement_id', e.target.value)}> value={form.investissement_id}
<option value="">—</option> onChange={v => onFieldChange('investissement_id', v)}
<option value="BONUS_PARRAINAGE">🎁 Bonus Parrainage</option> options={investissementOptions}
<option value="BONUS_PLATEFORME">🏆 Bonus Plateforme</option> />
<optgroup label="──────────────────" />
{investissementOptions.map(i =>
<option key={i.id} value={i.id}>{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})</option>
)}
</select>
</div> </div>
)} )}
{!locked && isBonus && ( {!locked && isBonus && (