Amélioration UI
This commit is contained in:
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in new issue
Block a user