import { useState, useEffect } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { DEFAULT_CONFIG } from '../../hooks/useCopySubscriptions';
import type { CopyConfig, AllocMode } from '../../hooks/useCopySubscriptions';
import { useT } from '../../i18n/LanguageContext';
import '../../redesign/editorial.css';
const ALLOC_MODES: { key: AllocMode; labelKey: string }[] = [
{ key: 'percent', labelKey: 'ccm.modePercent' },
{ key: 'fixed', labelKey: 'ccm.modeFixed' },
{ key: 'proportional', labelKey: 'ccm.modeProp' },
];
const CATEGORIES = ['Sports', 'Crypto', 'Politics', 'Economy', 'Pop Culture'];
// One-time risk acceptance, persisted locally PER USER (so a second tester on a shared
// browser still gets the disclaimer). Bump the suffix to re-prompt everyone.
const RISK_KEY_BASE = 'polycopy_risk_accepted_v1';
// Number input that allows fully clearing the field while typing (standard UX).
// Holds a local string buffer; commits a number on change, reverts to last value on empty blur.
function NumInput({ value, onChange, className, min, max }: {
value: number;
onChange: (v: number) => void;
className?: string;
min?: number;
max?: number;
}) {
const [text, setText] = useState(String(value));
useEffect(() => { setText(String(value)); }, [value]);
return (
<input
type="number"
min={min}
max={max}
className={className}
value={text}
onChange={e => {
setText(e.target.value);
if (e.target.value !== '') {
// clamp to min/max so out-of-range / negative input can't reach config (server also validates)
let v = Number(e.target.value);
if (Number.isFinite(v)) {
if (min != null) v = Math.max(min, v);
if (max != null) v = Math.min(max, v);
onChange(v);
}
}
}}
onBlur={() => { if (text === '') setText(String(value)); }}
/>
);
}
interface Props {
leaderLabel: string;
initial?: CopyConfig;
editing?: boolean;
onSave: (config: CopyConfig) => void;
onClose: () => void;
}
export default function CopyConfigModal({ leaderLabel, initial, editing, onSave, onClose }: Props) {
const { t, lang } = useT();
const { user } = usePrivy();
const riskKey = `${RISK_KEY_BASE}:${(user?.id || 'anon').toLowerCase()}`;
const [c, setC] = useState<CopyConfig>(initial ?? DEFAULT_CONFIG);
const [confirming, setConfirming] = useState(false);
const [accepted, setAccepted] = useState(false);
const set = <K extends keyof CopyConfig>(k: K, v: CopyConfig[K]) => setC(p => ({ ...p, [k]: v }));
// New subscriptions must pass a one-time (per-user) risk disclosure; editing an existing one skips it.
const handleStart = () => {
if (editing || localStorage.getItem(riskKey) === '1') { onSave(c); return; }
setConfirming(true);
};
const confirmRisk = () => {
try { localStorage.setItem(riskKey, '1'); } catch { /* ignore */ }
onSave(c);
};
const toggleCat = (cat: string) =>
set('categories', c.categories.includes(cat)
? c.categories.filter(x => x !== cat)
: [...c.categories, cat]);
return (
<div className="ccm-overlay pk-ccm" onClick={onClose}>
<div className="ccm-modal" onClick={e => e.stopPropagation()}>
{confirming ? (
<>
<div className="ccm-head">
<span className="ccm-title">{t('risk.title')}</span>
<button className="ccm-x" onClick={() => setConfirming(false)}>✕</button>
</div>
<div className="ccm-body">
<p className="ccm-hint">{t('risk.p1')}</p>
<p className="ccm-hint">{t('risk.p2')}</p>
<p className="ccm-hint">{t('risk.p3')}</p>
<p className="ccm-hint">{t('risk.p4')}</p>
<label className="ccm-field ccm-field-row" style={{ gap: 8, cursor: 'pointer' }}>
<input type="checkbox" checked={accepted} onChange={e => setAccepted(e.target.checked)} />
<span className="ccm-label">{t('risk.accept')}</span>
</label>
</div>
<div className="ccm-foot">
<button className="ccm-cancel" onClick={() => setConfirming(false)}>{t('ccm.cancel')}</button>
<button className="ccm-save" disabled={!accepted} onClick={confirmRisk}>{t('risk.confirm')}</button>
</div>
</>
) : (
<>
<div className="ccm-head">
<span className="ccm-title">{editing ? t('ccm.titleEdit') : t('ccm.titleNew')} · {leaderLabel}</span>
<button className="ccm-x" onClick={onClose}>✕</button>
</div>
<div className="ccm-body">
{/* Direction: COPY (follow) vs FADE (bet against) — counter-trading, chunk B */}
<div className="ccm-field">
<label className="ccm-label">{lang === 'ru' ? 'Режим' : 'Mode'}</label>
<div className="ccm-seg">
<button className={`ccm-seg-btn ${(c.direction ?? 'copy') === 'copy' ? 'active' : ''}`}
onClick={() => set('direction', 'copy')}>⇄ COPY</button>
<button className={`ccm-seg-btn pk-seg-fade ${c.direction === 'fade' ? 'active' : ''}`}
onClick={() => set('direction', 'fade')}>💀 FADE</button>
</div>
{c.direction === 'fade' && (
<>
<p className="ccm-hint">{lang === 'ru'
? 'Лидер покупает YES@p → ты покупаешь NO@(1−p). Ставка ПРОТИВ лидера.'
: 'Leader buys YES@p → you buy NO@(1−p). Bet AGAINST the leader.'}</p>
<label className="ccm-label" style={{ marginTop: 10 }}>{lang === 'ru'
? 'Risk-band — не фейдить крайних фаворитов' : 'Risk band — don’t fade extreme favorites'}</label>
<div className="ccm-inline">
<NumInput min={1} max={49} className="ccm-input"
value={Math.round((c.fadeMin ?? 0.20) * 100)}
onChange={v => set('fadeMin', Math.min(0.49, Math.max(0.01, v / 100)))} />
<span className="ccm-unit">¢ min</span>
<NumInput min={51} max={99} className="ccm-input"
value={Math.round((c.fadeMax ?? 0.80) * 100)}
onChange={v => set('fadeMax', Math.min(0.99, Math.max(0.51, v / 100)))} />
<span className="ccm-unit">¢ max</span>
</div>
<p className="ccm-hint">{lang === 'ru'
? 'Фейдим только если цена лидера в этом диапазоне (по умолч. 20–80¢).'
: 'Fade only when the leader price is in this range (default 20–80¢).'}</p>
</>
)}
</div>
{/* Аллокация */}
<div className="ccm-field">
<label className="ccm-label">{t('ccm.alloc')}</label>
<div className="ccm-seg">
{ALLOC_MODES.map(m => (
<button key={m.key}
className={`ccm-seg-btn ${c.allocMode === m.key ? 'active' : ''}`}
onClick={() => set('allocMode', m.key)}>
{t(m.labelKey)}
</button>
))}
</div>
{c.allocMode !== 'proportional' && (
<div className="ccm-inline">
<NumInput min={0} className="ccm-input"
value={c.allocValue}
onChange={v => set('allocValue', v)} />
<span className="ccm-unit">{c.allocMode === 'percent' ? t('ccm.unitPercent') : t('ccm.unitDollar')}</span>
</div>
)}
{c.allocMode === 'proportional' && (
<p className="ccm-hint">{t('ccm.propHint')}</p>
)}
</div>
{/* Лимиты */}
<div className="ccm-row2">
<div className="ccm-field">
<label className="ccm-label">{t('ccm.maxTrade')}</label>
<div className="ccm-inline">
<span className="ccm-unit">$</span>
<NumInput min={0} className="ccm-input"
value={c.maxPerTrade}
onChange={v => set('maxPerTrade', v)} />
</div>
</div>
<div className="ccm-field">
<label className="ccm-label">{t('ccm.maxExp')}</label>
<div className="ccm-inline">
<span className="ccm-unit">$</span>
<NumInput min={0} className="ccm-input"
value={c.maxExposure}
onChange={v => set('maxExposure', v)} />
</div>
</div>
</div>
{/* Категории */}
<div className="ccm-field">
<label className="ccm-label">{t('ccm.categories')} <span className="ccm-muted">{t('ccm.catEmpty')}</span></label>
<div className="ccm-chips">
{CATEGORIES.map(cat => (
<button key={cat}
className={`ccm-chip ${c.categories.includes(cat) ? 'active' : ''}`}
onClick={() => toggleCat(cat)}>
{cat}
</button>
))}
</div>
</div>
{/* Диапазон цен */}
<div className="ccm-field">
<label className="ccm-label">{t('ccm.priceRange')} <span className="ccm-muted">{t('ccm.noLongshots')}</span></label>
<div className="ccm-inline">
<NumInput min={1} max={99} className="ccm-input ccm-input-sm"
value={c.priceMin} onChange={v => set('priceMin', v)} />
<span className="ccm-unit">¢ —</span>
<NumInput min={1} max={99} className="ccm-input ccm-input-sm"
value={c.priceMax} onChange={v => set('priceMax', v)} />
<span className="ccm-unit">¢</span>
</div>
</div>
{/* Зеркалить выходы */}
<div className="ccm-field ccm-field-row">
<label className="ccm-label">{t('ccm.mirror')}</label>
<button className={`ccm-switch ${c.mirrorExits ? 'on' : ''}`}
onClick={() => set('mirrorExits', !c.mirrorExits)}>
<span className="ccm-switch-dot" />
</button>
</div>
{/* Стоп по просадке */}
<div className="ccm-field">
<label className="ccm-label">{t('ccm.drawdown')} <span className="ccm-muted">{t('ccm.drawdownOff')}</span></label>
<div className="ccm-inline">
<NumInput min={0} max={100} className="ccm-input ccm-input-sm"
value={c.drawdownStop} onChange={v => set('drawdownStop', v)} />
<span className="ccm-unit">%</span>
</div>
</div>
</div>
<div className="ccm-foot">
<button className="ccm-cancel" onClick={onClose}>{t('ccm.cancel')}</button>
<button className="ccm-save" onClick={handleStart}>
{editing ? t('ccm.save') : t('ccm.start')}
</button>
</div>
</>
)}
</div>
</div>
);
}
📜 Git History
b9c19bfchore(poli): reconcile local Flow/Insider/manual-trade work with deployed state3 days ago
1587787feat(poli): chunk B3 — FADE direction + risk-band in CopyConfigModal10 days ago
edefc19feat(poli): editorial Copy Config modal reskin (chunk 6)11 days ago
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...