refactor(ui): redesign manual entry form
This commit is contained in:
parent
2a23f88334
commit
f598084f6a
@ -5,17 +5,15 @@ import { tradingRuntime } from '../lib/runtime';
|
||||
import { createManualEntry, updateManualEntry } from '../lib/manualEntriesApi';
|
||||
import { getPlatformAccessToken } from '../lib/authSession';
|
||||
import { createRequestId } from '../../../shared/request-id.js';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Button, Checkbox, Input } from './ui/Primitives';
|
||||
|
||||
interface EntryFormProps {
|
||||
onSuccess: () => void;
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const tableInputClass = 'h-9 rounded border-[var(--border)] bg-[var(--input)] px-2 py-1 text-sm';
|
||||
const numericInputClass = `${tableInputClass} font-mono text-right`;
|
||||
const checkboxLabelClass = 'flex cursor-pointer items-center text-[10px] text-[var(--muted-foreground)] hover:text-[var(--foreground)]';
|
||||
const numericInputClass = 'font-mono text-right';
|
||||
const checkboxLabelClass = 'flex min-h-10 cursor-pointer items-center gap-2 rounded-[var(--bl-radius-control)] border border-[var(--border)] bg-[var(--card-elevated)] px-3 text-sm text-[var(--foreground)] transition hover:border-[var(--primary)]';
|
||||
|
||||
export function EntryForm({ onSuccess, initialData }: EntryFormProps) {
|
||||
const { user } = useAuth();
|
||||
@ -180,210 +178,93 @@ export function EntryForm({ onSuccess, initialData }: EntryFormProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="table-container">
|
||||
<table className="pro-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '15%' }}>Symbol</th>
|
||||
<th style={{ width: '10%' }}>Tag</th>
|
||||
<th style={{ width: '15%' }}>Options</th>
|
||||
<th style={{ width: '12%' }} className="text-right">Buy Price</th>
|
||||
<th style={{ width: '10%' }} className="text-right">Qty</th>
|
||||
<th style={{ width: '10%' }} className="text-right">Stop Loss</th>
|
||||
<th style={{ width: '10%' }} className="text-right">Take Profit</th>
|
||||
<th style={{ width: '12%' }}>Buy Time</th>
|
||||
<th style={{ width: '10%' }} className="text-right">Exit Price</th>
|
||||
<th style={{ width: '11%' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
{/* Symbol */}
|
||||
<td>
|
||||
<Input
|
||||
type="text"
|
||||
name="symbol"
|
||||
required
|
||||
value={formData.symbol}
|
||||
onChange={handleChange}
|
||||
placeholder="BTC/USD"
|
||||
className={tableInputClass}
|
||||
/>
|
||||
</td>
|
||||
<form onSubmit={handleSubmit} className="space-y-5 rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card)] p-5 shadow-[var(--bl-shadow-card)]">
|
||||
<section className="rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-bold text-[var(--foreground)]">Instrument</div>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--muted-foreground)]">Name the opportunity and choose whether it is a watch item, trade journal entry, or crypto setup.</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Symbol</span>
|
||||
<Input type="text" name="symbol" required value={formData.symbol} onChange={handleChange} placeholder="BTC/USD" />
|
||||
</label>
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Tag</span>
|
||||
<Input type="text" name="label" value={formData.label} onChange={handleChange} placeholder="Swing" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
||||
<div className={checkboxLabelClass} title="Places actual order on Bot">
|
||||
<Checkbox name="execute_order" checked={formData.execute_order} onChange={handleChange} label="Execute" />
|
||||
</div>
|
||||
<div className={checkboxLabelClass} title="Marks as Active Trade in Journal">
|
||||
<Checkbox name="is_real_trade" checked={formData.is_real_trade} onChange={handleChange} label="Trade" />
|
||||
</div>
|
||||
<div className={checkboxLabelClass}>
|
||||
<Checkbox name="is_crypto" checked={formData.is_crypto} onChange={handleChange} label="Crypto" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Label */}
|
||||
<td>
|
||||
<Input
|
||||
type="text"
|
||||
name="label"
|
||||
value={formData.label}
|
||||
onChange={handleChange}
|
||||
placeholder="Swing"
|
||||
className={tableInputClass}
|
||||
/>
|
||||
</td>
|
||||
<section className="rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-bold text-[var(--foreground)]">Pricing and risk</div>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--muted-foreground)]">Capture the planned entry, size, stop loss, take profit, and optional exit price.</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Buy price</span>
|
||||
<Input type="number" name="buy_price" step="any" value={formData.buy_price} onChange={handleChange} className={`${numericInputClass} text-[var(--bl-success)]`} placeholder="0.00" />
|
||||
</label>
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Quantity</span>
|
||||
<Input type="number" name="quantity" step="any" value={formData.quantity} onChange={handleChange} className={numericInputClass} placeholder="0" />
|
||||
</label>
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Buy time</span>
|
||||
<Input type="datetime-local" name="buy_time" value={formData.buy_time ? new Date(formData.buy_time).toISOString().slice(0, 16) : ''} onChange={(e) => setFormData(prev => ({ ...prev, buy_time: new Date(e.target.value).toISOString() }))} className="font-mono" />
|
||||
</label>
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Stop loss</span>
|
||||
<Input type="number" name="drop_threshold_for_buy" step="any" value={formData.drop_threshold_for_buy} onChange={handleChange} className={`${numericInputClass} text-[var(--bl-danger)]`} placeholder="SL" />
|
||||
</label>
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Take profit</span>
|
||||
<Input type="number" name="gain_threshold_for_sell" step="any" value={formData.gain_threshold_for_sell} onChange={handleChange} className={`${numericInputClass} text-[var(--bl-info)]`} placeholder="TP" />
|
||||
</label>
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Exit price</span>
|
||||
<Input type="number" name="sell_price" step="any" value={formData.sell_price} onChange={handleChange} className={`${numericInputClass} text-[var(--bl-info-strong)]`} placeholder="Exit" />
|
||||
{formData.sell_price && formData.buy_price && (
|
||||
<span className={`text-xs font-bold ${(Number(formData.sell_price) - Number(formData.buy_price)) * Number(formData.quantity || 0) >= 0 ? 'text-[var(--bl-success)]' : 'text-[var(--bl-danger)]'}`}>
|
||||
Estimated P/L ${((Number(formData.sell_price) - Number(formData.buy_price)) * Number(formData.quantity || 1)).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Options (Real/Crypto) */}
|
||||
<td>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="flex cursor-pointer items-center text-xs text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300" title="Places actual order on Bot">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="execute_order"
|
||||
checked={formData.execute_order}
|
||||
onChange={handleChange}
|
||||
className="mr-1.5 accent-yellow-500"
|
||||
/>
|
||||
Execute
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className={checkboxLabelClass} title="Marks as Active Trade in Journal">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_real_trade"
|
||||
checked={formData.is_real_trade}
|
||||
onChange={handleChange}
|
||||
className="mr-1 accent-blue-500 scale-75"
|
||||
/>
|
||||
Trade
|
||||
</label>
|
||||
<label className={checkboxLabelClass}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_crypto"
|
||||
checked={formData.is_crypto}
|
||||
onChange={handleChange}
|
||||
className="mr-1 accent-purple-500 scale-75"
|
||||
/>
|
||||
Crypto
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<section className="rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
|
||||
<label className="ux-field-stack">
|
||||
<span className="ux-field-label">Strategy notes</span>
|
||||
<Input type="text" name="notes" value={formData.notes} onChange={handleChange} placeholder="Add strategy notes here..." />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{/* Buy Price */}
|
||||
<td>
|
||||
<Input
|
||||
type="number"
|
||||
name="buy_price"
|
||||
step="any"
|
||||
value={formData.buy_price}
|
||||
onChange={handleChange}
|
||||
className={`${numericInputClass} text-emerald-600 dark:text-emerald-400`}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* Qty */}
|
||||
<td>
|
||||
<Input
|
||||
type="number"
|
||||
name="quantity"
|
||||
step="any"
|
||||
value={formData.quantity}
|
||||
onChange={handleChange}
|
||||
className={numericInputClass}
|
||||
placeholder="0"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* SL */}
|
||||
<td>
|
||||
<Input
|
||||
type="number"
|
||||
name="drop_threshold_for_buy"
|
||||
step="any"
|
||||
value={formData.drop_threshold_for_buy}
|
||||
onChange={handleChange}
|
||||
className={`${numericInputClass} text-red-600 dark:text-red-400`}
|
||||
placeholder="SL"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* TP */}
|
||||
<td>
|
||||
<Input
|
||||
type="number"
|
||||
name="gain_threshold_for_sell"
|
||||
step="any"
|
||||
value={formData.gain_threshold_for_sell}
|
||||
onChange={handleChange}
|
||||
className={`${numericInputClass} text-blue-600 dark:text-blue-400`}
|
||||
placeholder="TP"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* Time */}
|
||||
<td>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
name="buy_time"
|
||||
value={formData.buy_time ? new Date(formData.buy_time).toISOString().slice(0, 16) : ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, buy_time: new Date(e.target.value).toISOString() }))}
|
||||
className="h-9 rounded border-[var(--border)] bg-[var(--input)] px-2 py-1 font-mono text-xs"
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* Sell Price (Exit) */}
|
||||
<td className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
name="sell_price"
|
||||
step="any"
|
||||
value={formData.sell_price}
|
||||
onChange={handleChange}
|
||||
className={`${numericInputClass} text-purple-600 dark:text-purple-400`}
|
||||
placeholder="Exit"
|
||||
/>
|
||||
{formData.sell_price && formData.buy_price && (
|
||||
<div className={`absolute top-0 right-0 -mt-3 text-[9px] font-bold ${(Number(formData.sell_price) - Number(formData.buy_price)) * Number(formData.quantity || 0) >= 0
|
||||
? 'text-green-500'
|
||||
: 'text-red-500'
|
||||
}`}>
|
||||
${((Number(formData.sell_price) - Number(formData.buy_price)) * Number(formData.quantity || 1)).toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{initialData && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSuccess}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 text-xs underline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
{initialData ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Notes Row */}
|
||||
<tr>
|
||||
<td colSpan={10} className="border-t border-[var(--border)] pt-2">
|
||||
<Input
|
||||
type="text"
|
||||
name="notes"
|
||||
value={formData.notes}
|
||||
onChange={handleChange}
|
||||
placeholder="Add strategy notes here..."
|
||||
className="h-9 border-transparent bg-transparent px-0 text-xs italic shadow-none focus:border-transparent focus:ring-0"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col gap-3 rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card-elevated)] p-4 md:flex-row md:items-center md:justify-between">
|
||||
<p className="text-sm leading-6 text-[var(--muted-foreground)]">Review the symbol, sizing, and risk fields before saving this manual entry.</p>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{initialData && (
|
||||
<Button type="button" onClick={onSuccess} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit">
|
||||
{initialData ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -49,6 +49,10 @@ export interface BadgeProps extends Omit<CommonBadgeProps, 'variant'> {
|
||||
variant?: ProductBadgeVariant;
|
||||
}
|
||||
|
||||
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
export type ProductStatus =
|
||||
| 'active'
|
||||
| 'approved'
|
||||
@ -208,6 +212,27 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
({ className, label, id, ...props }, ref) => {
|
||||
const generatedId = React.useId();
|
||||
const checkboxId = id ?? (label ? `checkbox-${generatedId}` : undefined);
|
||||
return (
|
||||
<label className="inline-flex items-center gap-2 text-sm text-[var(--foreground)]">
|
||||
<input
|
||||
ref={ref}
|
||||
id={checkboxId}
|
||||
type="checkbox"
|
||||
className={cn('h-4 w-4 rounded border-[var(--border)] bg-[var(--card)] text-[var(--primary)] focus-visible:ring-2 focus-visible:ring-[var(--ring-soft)]', className)}
|
||||
{...props}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Checkbox.displayName = 'Checkbox';
|
||||
|
||||
export function Badge({ variant = 'neutral', ...props }: BadgeProps) {
|
||||
return <CommonBadge variant={badgeVariantFor(variant)} {...props} />;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user