// Rent filter sheet — dual-handle range slider, max ¥1,000,000 const { useState: useStateRent, useRef: useRefRent, useEffect: useEffectRent } = React; const MIN_RENT = 0; const MAX_RENT = 1000000; function RentSheet({ open, onClose, value, onChange }) { // value: {min, max} const [local, setLocal] = useStateRent(value); const trackRef = useRefRent(null); const dragging = useRefRent(null); useEffectRent(() => { if (open) setLocal(value); }, [open]); const pct = (v) => ((v - MIN_RENT) / (MAX_RENT - MIN_RENT)) * 100; const handlePointer = (e, handle) => { dragging.current = handle; e.preventDefault(); }; const onMove = (e) => { if (!dragging.current || !trackRef.current) return; const rect = trackRef.current.getBoundingClientRect(); const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; const ratio = Math.max(0, Math.min(1, x / rect.width)); let raw = MIN_RENT + ratio * (MAX_RENT - MIN_RENT); // Snap to 10,000 raw = Math.round(raw / 10000) * 10000; setLocal(prev => { if (dragging.current === 'min') { return { ...prev, min: Math.min(raw, prev.max - 10000) }; } return { ...prev, max: Math.max(raw, prev.min + 10000) }; }); }; const onUp = () => { dragging.current = null; }; useEffectRent(() => { if (!open) return; const moveHandler = (e) => onMove(e); const upHandler = () => onUp(); window.addEventListener('mousemove', moveHandler); window.addEventListener('mouseup', upHandler); window.addEventListener('touchmove', moveHandler, { passive: false }); window.addEventListener('touchend', upHandler); return () => { window.removeEventListener('mousemove', moveHandler); window.removeEventListener('mouseup', upHandler); window.removeEventListener('touchmove', moveHandler); window.removeEventListener('touchend', upHandler); }; }, [open]); if (!open) return null; // Quick presets const presets = [ { label: '〜¥8万', min: 0, max: 80000 }, { label: '¥8〜15万', min: 80000, max: 150000 }, { label: '¥15〜25万', min: 150000, max: 250000 }, { label: '¥25〜50万', min: 250000, max: 500000 }, { label: 'すべて', min: 0, max: 1000000 }, ]; const sheetContent = (
e.stopPropagation()}>
按租金筛选
{/* Value display */}
最低
¥ {local.min.toLocaleString()}
最高
¥ {local.max >= MAX_RENT ? '不限' : local.max.toLocaleString()}
{/* Histogram-ish bars for visual density */}
{Array.from({ length: 28 }).map((_, i) => { const barMin = (i / 28) * MAX_RENT; const barMax = ((i + 1) / 28) * MAX_RENT; const inside = barMax >= local.min && barMin <= local.max; // density curve: peak around 100k-200k const center = (barMin + barMax) / 2; const density = Math.exp(-Math.pow((center - 150000) / 180000, 2)) * 0.9 + 0.1; return (
); })}
{/* Slider track */}
handlePointer(e, 'min')} onTouchStart={e => handlePointer(e, 'min')} />
handlePointer(e, 'max')} onTouchStart={e => handlePointer(e, 'max')} />
¥0 ¥25万 ¥50万 ¥75万 ¥100万
{/* Presets */}
常用范围
{presets.map(p => { const active = local.min === p.min && local.max === p.max; return ( ); })}
); return ReactDOM.createPortal(sheetContent, document.body); } const rentStyles = { backdrop: { position: 'fixed', inset: 0, background: 'rgba(20,18,14,0.38)', display: 'flex', alignItems: 'flex-end', zIndex: 1000, animation: 'fadeIn 180ms ease', }, sheet: { width: '100%', background: '#FAF8F5', borderTopLeftRadius: 20, borderTopRightRadius: 20, display: 'flex', flexDirection: 'column', maxHeight: '82dvh', boxShadow: '0 -8px 32px rgba(0,0,0,0.12)', animation: 'slideUp 260ms cubic-bezier(.2,.8,.2,1)', }, grabber: { width: 36, height: 4, borderRadius: 2, background: '#D4CCBE', margin: '8px auto 0' }, header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 20px 6px', }, headerTitle: { fontSize: 17, fontWeight: 600, color: '#1A1A1A' }, closeBtn: { width: 32, height: 32, borderRadius: 16, border: 'none', background: '#EFEAE0', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', }, body: { padding: '8px 20px 4px' }, valueRow: { display: 'flex', alignItems: 'center', gap: 12, padding: '14px 0 18px', }, valueCol: { flex: 1 }, valueLabel: { fontSize: 11, color: '#8A7F72', marginBottom: 4, fontWeight: 500 }, valueNum: { display: 'flex', alignItems: 'baseline', gap: 2 }, yenMark: { fontSize: 14, color: '#6B6258', fontWeight: 500 }, valueBig: { fontSize: 26, fontWeight: 600, color: '#1A1A1A', fontFamily: '"Inter", system-ui, sans-serif', letterSpacing: '-0.01em', }, divider: { color: '#C4BBAA', fontSize: 18, paddingBottom: 2 }, histoRow: { display: 'flex', alignItems: 'flex-end', height: 44, marginBottom: 4, padding: '0 11px', }, trackWrap: { padding: '0 11px 8px' }, track: { position: 'relative', height: 4, background: '#E4DFD6', borderRadius: 2, margin: '14px 0', }, trackFill: { position: 'absolute', top: 0, bottom: 0, background: '#1A1A1A', borderRadius: 2, }, handle: { position: 'absolute', top: '50%', width: 22, height: 22, borderRadius: 11, background: '#FFFFFF', border: '2px solid #1A1A1A', transform: 'translate(-50%, -50%)', cursor: 'grab', touchAction: 'none', boxShadow: '0 2px 6px rgba(0,0,0,0.15)', }, tickRow: { display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#8A7F72', marginTop: 2, fontWeight: 500, }, presetLabel: { fontSize: 11, color: '#8A7F72', fontWeight: 500, padding: '16px 0 8px', letterSpacing: '0.04em', }, presetGrid: { display: 'flex', flexWrap: 'wrap', gap: 8, paddingBottom: 8 }, presetBtn: { padding: '10px 14px', borderRadius: 999, border: '1px solid #E4DFD6', fontSize: 13, fontWeight: 500, fontFamily: 'inherit', cursor: 'pointer', }, footer: { display: 'flex', gap: 10, padding: '12px 20px 20px', borderTop: '1px solid #EDE6D9', }, resetBtn: { padding: '15px 20px', borderRadius: 14, border: '1px solid #E4DFD6', background: 'transparent', color: '#1A1A1A', fontSize: 14, fontWeight: 500, fontFamily: 'inherit', cursor: 'pointer', }, applyBtn: { flex: 1, padding: '15px', borderRadius: 14, border: 'none', background: '#1A1A1A', color: '#FAF8F5', fontSize: 15, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer', }, }; Object.assign(window, { RentSheet, MIN_RENT, MAX_RENT });