Skip to content

Commit 194fdde

Browse files
committed
Fixed - Create Vsum page #43
1 parent fbf08ba commit 194fdde

File tree

6 files changed

+384
-36
lines changed

6 files changed

+384
-36
lines changed

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { SidebarTabs } from './ui/SidebarTabs';
1414
export { ConfirmDialog } from './ui/ConfirmDialog';
1515
export { CreateModelModal } from './ui/CreateModelModal';
1616
export { KeywordTagsInput } from './ui/KeywordTagsInput';
17+
export { ToastProvider, useToast } from './ui/ToastProvider';
1718

1819

1920
// Auth components
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import React, { useState } from 'react';
2+
import { apiService } from '../../services/api';
3+
import { useToast } from './ToastProvider';
4+
5+
interface CreateVsumModalProps {
6+
isOpen: boolean;
7+
onClose: () => void;
8+
onSuccess?: (vsum: any) => void;
9+
}
10+
11+
const modalOverlayStyle: React.CSSProperties = {
12+
position: 'fixed',
13+
top: 0,
14+
left: 0,
15+
right: 0,
16+
bottom: 0,
17+
background: 'rgba(0, 0, 0, 0.5)',
18+
display: 'flex',
19+
alignItems: 'center',
20+
justifyContent: 'center',
21+
zIndex: 1000,
22+
};
23+
24+
const modalStyle: React.CSSProperties = {
25+
background: '#ffffff',
26+
borderRadius: '8px',
27+
padding: '24px',
28+
width: '480px',
29+
maxWidth: '90vw',
30+
maxHeight: '85vh',
31+
overflow: 'auto',
32+
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.2)',
33+
border: '1px solid #d1ecf1',
34+
fontFamily: 'Georgia, serif',
35+
};
36+
37+
const headerStyle: React.CSSProperties = {
38+
display: 'flex',
39+
justifyContent: 'space-between',
40+
alignItems: 'center',
41+
marginBottom: '16px',
42+
paddingBottom: '12px',
43+
borderBottom: '2px solid #3498db',
44+
};
45+
46+
const titleStyle: React.CSSProperties = {
47+
fontSize: '20px',
48+
fontWeight: 700,
49+
color: '#2c3e50',
50+
margin: 0,
51+
fontFamily: 'Georgia, serif',
52+
};
53+
54+
const closeButtonStyle: React.CSSProperties = {
55+
background: 'transparent',
56+
border: '1px solid #dee2e6',
57+
fontSize: '16px',
58+
color: '#6c757d',
59+
cursor: 'pointer',
60+
padding: '6px 10px',
61+
borderRadius: '6px',
62+
};
63+
64+
const formGroupStyle: React.CSSProperties = {
65+
marginBottom: '14px',
66+
};
67+
68+
const labelStyle: React.CSSProperties = {
69+
display: 'block',
70+
fontSize: '13px',
71+
fontWeight: 600,
72+
color: '#2c3e50',
73+
marginBottom: '6px',
74+
fontFamily: 'Georgia, serif',
75+
};
76+
77+
const inputStyle: React.CSSProperties = {
78+
width: '100%',
79+
padding: '10px 12px',
80+
border: '1px solid #ced4da',
81+
borderRadius: '6px',
82+
fontSize: '13px',
83+
boxSizing: 'border-box',
84+
background: '#ffffff',
85+
fontFamily: 'Georgia, serif',
86+
};
87+
88+
const buttonRowStyle: React.CSSProperties = {
89+
display: 'flex',
90+
gap: '8px',
91+
justifyContent: 'flex-end',
92+
marginTop: '12px',
93+
};
94+
95+
const primaryButtonStyle: React.CSSProperties = {
96+
padding: '10px 12px',
97+
border: '1px solid #dee2e6',
98+
borderRadius: '6px',
99+
background: '#e7f5ff',
100+
cursor: 'pointer',
101+
fontWeight: 700,
102+
};
103+
104+
const secondaryButtonStyle: React.CSSProperties = {
105+
padding: '10px 12px',
106+
border: '1px solid #dee2e6',
107+
borderRadius: '6px',
108+
background: '#f8f9fa',
109+
cursor: 'pointer',
110+
fontWeight: 600,
111+
};
112+
113+
const errorMessageStyle: React.CSSProperties = {
114+
padding: '8px 12px',
115+
margin: '8px 0',
116+
borderRadius: '6px',
117+
fontSize: '12px',
118+
fontWeight: '500',
119+
backgroundColor: '#f8d7da',
120+
color: '#721c24',
121+
border: '1px solid #f5c6cb',
122+
};
123+
124+
export const CreateVsumModal: React.FC<CreateVsumModalProps> = ({ isOpen, onClose, onSuccess }) => {
125+
const [name, setName] = useState('');
126+
const [description, setDescription] = useState('');
127+
const [loading, setLoading] = useState(false);
128+
const [error, setError] = useState('');
129+
const { showSuccess, showError } = useToast();
130+
131+
if (!isOpen) return null;
132+
133+
const handleSubmit = async () => {
134+
const trimmedName = name.trim();
135+
if (!trimmedName) {
136+
setError('Name is required');
137+
return;
138+
}
139+
setLoading(true);
140+
setError('');
141+
try {
142+
const res = await apiService.createVsum({ name: trimmedName, description: description.trim() || undefined });
143+
onSuccess?.(res.data);
144+
showSuccess('Vsum successfully created');
145+
setName('');
146+
setDescription('');
147+
onClose();
148+
} catch (e) {
149+
const msg = e instanceof Error ? e.message : 'Failed to create vSUM';
150+
setError(msg);
151+
showError(msg);
152+
} finally {
153+
setLoading(false);
154+
}
155+
};
156+
157+
const handleClose = () => {
158+
if (loading) return;
159+
setError('');
160+
setName('');
161+
setDescription('');
162+
onClose();
163+
};
164+
165+
return (
166+
<div style={modalOverlayStyle} onClick={handleClose}>
167+
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
168+
<div style={headerStyle}>
169+
<h3 style={titleStyle}>Create vSUM</h3>
170+
<button style={closeButtonStyle} onClick={handleClose}>
171+
×
172+
</button>
173+
</div>
174+
175+
{error && <div style={errorMessageStyle}>{error}</div>}
176+
177+
<div style={formGroupStyle}>
178+
<label style={labelStyle}>Name *</label>
179+
<input
180+
placeholder="Enter name"
181+
value={name}
182+
onChange={(e) => setName(e.target.value)}
183+
style={inputStyle}
184+
disabled={loading}
185+
/>
186+
</div>
187+
188+
<div style={formGroupStyle}>
189+
<label style={labelStyle}>Description</label>
190+
<textarea
191+
placeholder="Optional description"
192+
value={description}
193+
onChange={(e) => setDescription(e.target.value)}
194+
style={{ ...inputStyle, minHeight: 80, resize: 'vertical' }}
195+
disabled={loading}
196+
/>
197+
</div>
198+
199+
<div style={buttonRowStyle}>
200+
<button style={secondaryButtonStyle} onClick={handleClose} disabled={loading}>Cancel</button>
201+
<button style={primaryButtonStyle} onClick={handleSubmit} disabled={loading || !name.trim()}>
202+
{loading ? 'Creating...' : 'Create vSUM'}
203+
</button>
204+
</div>
205+
</div>
206+
</div>
207+
);
208+
};
209+
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
2+
3+
type ToastType = 'success' | 'error' | 'info';
4+
5+
interface ToastItem {
6+
id: number;
7+
type: ToastType;
8+
message: string;
9+
durationMs: number;
10+
}
11+
12+
interface ToastContextValue {
13+
showSuccess: (message: string, durationMs?: number) => void;
14+
showError: (message: string, durationMs?: number) => void;
15+
showInfo: (message: string, durationMs?: number) => void;
16+
}
17+
18+
const ToastContext = createContext<ToastContextValue | undefined>(undefined);
19+
20+
const containerStyle: React.CSSProperties = {
21+
position: 'fixed',
22+
top: 16,
23+
right: 16,
24+
display: 'flex',
25+
flexDirection: 'column',
26+
gap: 8,
27+
zIndex: 2000
28+
};
29+
30+
const baseToastStyle: React.CSSProperties = {
31+
minWidth: 280,
32+
maxWidth: 420,
33+
padding: '10px 12px',
34+
borderRadius: 8,
35+
border: '1px solid transparent',
36+
boxShadow: '0 10px 20px rgba(0,0,0,0.15)',
37+
color: '#1f2937',
38+
backgroundColor: '#ffffff',
39+
fontSize: 13,
40+
fontWeight: 500,
41+
display: 'flex',
42+
alignItems: 'flex-start',
43+
gap: 10
44+
};
45+
46+
function getToastStyle(type: ToastType): React.CSSProperties {
47+
if (type === 'success') {
48+
return {
49+
...baseToastStyle,
50+
borderColor: '#c6f6d5',
51+
backgroundColor: '#f0fff4'
52+
};
53+
}
54+
if (type === 'error') {
55+
return {
56+
...baseToastStyle,
57+
borderColor: '#feb2b2',
58+
backgroundColor: '#fff5f5'
59+
};
60+
}
61+
return {
62+
...baseToastStyle,
63+
borderColor: '#bee3f8',
64+
backgroundColor: '#ebf8ff'
65+
};
66+
}
67+
68+
const titleStyle: React.CSSProperties = {
69+
margin: 0,
70+
fontSize: 13,
71+
lineHeight: '18px',
72+
color: '#111827'
73+
};
74+
75+
const closeButtonStyle: React.CSSProperties = {
76+
marginLeft: 'auto',
77+
background: 'transparent',
78+
border: 'none',
79+
cursor: 'pointer',
80+
color: '#6b7280',
81+
fontSize: 14
82+
};
83+
84+
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
85+
const [toasts, setToasts] = useState<ToastItem[]>([]);
86+
const idRef = useRef(1);
87+
88+
const remove = useCallback((id: number) => {
89+
setToasts((prev) => prev.filter((t) => t.id !== id));
90+
}, []);
91+
92+
const push = useCallback((type: ToastType, message: string, durationMs = 3000) => {
93+
const id = idRef.current++;
94+
const toast: ToastItem = { id, type, message, durationMs };
95+
setToasts((prev) => [toast, ...prev]);
96+
window.setTimeout(() => remove(id), durationMs);
97+
}, [remove]);
98+
99+
const value = useMemo<ToastContextValue>(() => ({
100+
showSuccess: (message: string, durationMs?: number) => push('success', message, durationMs),
101+
showError: (message: string, durationMs?: number) => push('error', message, durationMs),
102+
showInfo: (message: string, durationMs?: number) => push('info', message, durationMs)
103+
}), [push]);
104+
105+
return (
106+
<ToastContext.Provider value={value}>
107+
{children}
108+
<div style={containerStyle} aria-live="polite" aria-atomic="true">
109+
{toasts.map((t) => (
110+
<div key={t.id} style={getToastStyle(t.type)} role="status">
111+
<p style={titleStyle}>{t.message}</p>
112+
<button style={closeButtonStyle} onClick={() => remove(t.id)} aria-label="Close">×</button>
113+
</div>
114+
))}
115+
</div>
116+
</ToastContext.Provider>
117+
);
118+
};
119+
120+
export function useToast(): ToastContextValue {
121+
const ctx = useContext(ToastContext);
122+
if (!ctx) throw new Error('useToast must be used within a ToastProvider');
123+
return ctx;
124+
}
125+

0 commit comments

Comments
 (0)