174 lines
No EOL
6 KiB
TypeScript
174 lines
No EOL
6 KiB
TypeScript
import { Dialog, DialogContent, DialogHeader, DialogTitle, Button } from '@/components/ui';
|
|
import { useCallback } from 'react';
|
|
import { CreateExchangeRequest, AddExchangeDialogProps } from '../types';
|
|
import { validateExchangeForm } from '../utils/validation';
|
|
import { useFormValidation } from '../hooks/useFormValidation';
|
|
|
|
const initialFormData: CreateExchangeRequest = {
|
|
code: '',
|
|
name: '',
|
|
country: '',
|
|
currency: '',
|
|
active: true,
|
|
};
|
|
|
|
export function AddExchangeDialog({
|
|
isOpen,
|
|
onClose,
|
|
onCreateExchange,
|
|
}: AddExchangeDialogProps) {
|
|
const {
|
|
formData,
|
|
errors,
|
|
isSubmitting,
|
|
updateField,
|
|
handleSubmit,
|
|
reset,
|
|
} = useFormValidation(initialFormData, validateExchangeForm);
|
|
|
|
const onSubmit = useCallback(
|
|
async (data: CreateExchangeRequest) => {
|
|
await onCreateExchange({
|
|
...data,
|
|
code: data.code.toUpperCase(),
|
|
country: data.country.toUpperCase(),
|
|
currency: data.currency.toUpperCase(),
|
|
});
|
|
},
|
|
[onCreateExchange]
|
|
);
|
|
|
|
const handleFormSubmit = useCallback(
|
|
(e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
handleSubmit(onSubmit, onClose);
|
|
},
|
|
[handleSubmit, onSubmit, onClose]
|
|
);
|
|
|
|
const handleClose = useCallback(() => {
|
|
reset();
|
|
onClose();
|
|
}, [reset, onClose]);
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Add New Exchange</DialogTitle>
|
|
<p className="text-sm text-text-muted">
|
|
Create a new master exchange with no provider mappings
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleFormSubmit} className="space-y-4">
|
|
{/* Exchange Code */}
|
|
<div>
|
|
<label htmlFor="code" className="block text-sm font-medium text-text-primary mb-1">
|
|
Exchange Code *
|
|
</label>
|
|
<input
|
|
id="code"
|
|
type="text"
|
|
value={formData.code}
|
|
onChange={e => updateField('code', e.target.value)}
|
|
placeholder="e.g., NASDAQ, NYSE, TSX"
|
|
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono ${
|
|
errors.code ? 'border-danger' : 'border-border'
|
|
}`}
|
|
maxLength={10}
|
|
required
|
|
/>
|
|
{errors.code && <p className="text-xs text-danger mt-1">{errors.code}</p>}
|
|
</div>
|
|
|
|
{/* Exchange Name */}
|
|
<div>
|
|
<label htmlFor="name" className="block text-sm font-medium text-text-primary mb-1">
|
|
Exchange Name *
|
|
</label>
|
|
<input
|
|
id="name"
|
|
type="text"
|
|
value={formData.name}
|
|
onChange={e => updateField('name', e.target.value)}
|
|
placeholder="e.g., NASDAQ Stock Market, New York Stock Exchange"
|
|
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 ${
|
|
errors.name ? 'border-danger' : 'border-border'
|
|
}`}
|
|
maxLength={255}
|
|
required
|
|
/>
|
|
{errors.name && <p className="text-xs text-danger mt-1">{errors.name}</p>}
|
|
</div>
|
|
|
|
{/* Country */}
|
|
<div>
|
|
<label htmlFor="country" className="block text-sm font-medium text-text-primary mb-1">
|
|
Country Code *
|
|
</label>
|
|
<input
|
|
id="country"
|
|
type="text"
|
|
value={formData.country}
|
|
onChange={e => updateField('country', e.target.value)}
|
|
placeholder="e.g., US, CA, GB"
|
|
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono ${
|
|
errors.country ? 'border-danger' : 'border-border'
|
|
}`}
|
|
maxLength={2}
|
|
required
|
|
/>
|
|
{errors.country && <p className="text-xs text-danger mt-1">{errors.country}</p>}
|
|
</div>
|
|
|
|
{/* Currency */}
|
|
<div>
|
|
<label htmlFor="currency" className="block text-sm font-medium text-text-primary mb-1">
|
|
Currency Code *
|
|
</label>
|
|
<input
|
|
id="currency"
|
|
type="text"
|
|
value={formData.currency}
|
|
onChange={e => updateField('currency', e.target.value)}
|
|
placeholder="e.g., USD, EUR, CAD"
|
|
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono ${
|
|
errors.currency ? 'border-danger' : 'border-border'
|
|
}`}
|
|
maxLength={3}
|
|
required
|
|
/>
|
|
{errors.currency && <p className="text-xs text-danger mt-1">{errors.currency}</p>}
|
|
</div>
|
|
|
|
{/* Active Toggle */}
|
|
<div>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.active}
|
|
onChange={e => updateField('active', e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
<span className="text-sm text-text-primary">Active exchange</span>
|
|
</label>
|
|
<p className="text-xs text-text-muted mt-1">
|
|
Inactive exchanges won't be used for new symbol mappings
|
|
</p>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<Button type="button" variant="outline" onClick={handleClose} disabled={isSubmitting}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? 'Creating...' : 'Create Exchange'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |