EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
DomainRequestDetailModal.jsx
Go to the documentation of this file.
1import { useState } from 'react';
2import { apiFetch } from '../../../lib/api';
3import { notifySuccess, notifyError } from '../../../utils/notifications';
4
5export default function DomainRequestDetailModal({ request, onClose, onRequestProcessed }) {
6 const [processing, setProcessing] = useState(false);
7 const [approvalNotes, setApprovalNotes] = useState('');
8 const [selectedRegistrar, setSelectedRegistrar] = useState('cloudflare-registrar');
9
10 const handleApprove = async () => {
11 const registrarNames = {
12 cloudflare: 'Cloudflare (DNS-only)',
13 'cloudflare-registrar': 'Cloudflare Registrar',
14 enom: 'eNom',
15 moniker: 'Moniker'
16 };
17
18 if (!confirm(`Approve domain registration for ${request.domain_name}? This will attempt registration via ${registrarNames[selectedRegistrar]}.`)) {
19 return;
20 }
21
22 setProcessing(true);
23 try {
24 await apiFetch(`/domain-requests/${request.request_id}/approve`, {
25 method: 'POST',
26 body: JSON.stringify({
27 approval_notes: approvalNotes,
28 registrar: selectedRegistrar
29 })
30 });
31
32 await notifySuccess(
33 'Registration Approved',
34 `Domain registration for ${request.domain_name} has been approved and is being processed`
35 );
36
37 onRequestProcessed();
38 } catch (err) {
39 console.error('Error approving request:', err);
40 await notifyError('Approval Failed', err.message || 'Failed to approve registration request');
41 } finally {
42 setProcessing(false);
43 }
44 };
45
46 const handleDeny = async () => {
47 if (!approvalNotes.trim()) {
48 await notifyError('Notes Required', 'Please provide a reason for denying this request');
49 return;
50 }
51
52 if (!confirm(`Deny domain registration for ${request.domain_name}?`)) {
53 return;
54 }
55
56 setProcessing(true);
57 try {
58 await apiFetch(`/domain-requests/${request.request_id}/deny`, {
59 method: 'POST',
60 body: JSON.stringify({ approval_notes: approvalNotes })
61 });
62
63 await notifySuccess(
64 'Registration Denied',
65 `Domain registration for ${request.domain_name} has been denied`
66 );
67
68 onRequestProcessed();
69 } catch (err) {
70 console.error('Error denying request:', err);
71 await notifyError('Denial Failed', err.message || 'Failed to deny registration request');
72 } finally {
73 setProcessing(false);
74 }
75 };
76
77 const formatContact = (contact) => {
78 if (!contact) return null;
79 return (
80 <div style={{ fontSize: '0.9em', lineHeight: '1.6' }}>
81 <div><strong>{contact.firstName} {contact.lastName}</strong></div>
82 {contact.organization && <div>{contact.organization}</div>}
83 <div>{contact.email}</div>
84 <div>{contact.phone}</div>
85 <div>{contact.address}</div>
86 <div>{contact.city}, {contact.state} {contact.postalCode}</div>
87 <div>{contact.country}</div>
88 </div>
89 );
90 };
91
92 const formatDate = (dateString) => {
93 if (!dateString) return '-';
94 return new Date(dateString).toLocaleString('en-US', {
95 year: 'numeric',
96 month: 'short',
97 day: 'numeric',
98 hour: '2-digit',
99 minute: '2-digit'
100 });
101 };
102
103 const getStatusBadge = (status) => {
104 const statusColors = {
105 pending: { bg: '#fff3e0', text: '#e65100', label: 'Pending Review' },
106 approved: { bg: '#e8f5e9', text: '#2e7d32', label: 'Approved' },
107 denied: { bg: '#ffebee', text: '#c62828', label: 'Denied' },
108 registered: { bg: '#e3f2fd', text: '#1565c0', label: 'Successfully Registered' },
109 failed: { bg: '#fce4ec', text: '#ad1457', label: 'Registration Failed' }
110 };
111
112 const style = statusColors[status] || statusColors.pending;
113
114 return (
115 <span style={{
116 padding: '6px 14px',
117 borderRadius: '16px',
118 fontSize: '13px',
119 fontWeight: '500',
120 background: style.bg,
121 color: style.text
122 }}>
123 {style.label}
124 </span>
125 );
126 };
127
128 const isPending = request.status === 'pending';
129
130 return (
131 <div className="modal-overlay" onClick={onClose}>
132 <div className="modal-content large-modal" onClick={(e) => e.stopPropagation()}>
133 <div className="modal-header">
134 <div>
135 <h2>Domain Registration Request</h2>
136 <div style={{ marginTop: '8px' }}>{getStatusBadge(request.status)}</div>
137 </div>
138 <button className="modal-close" onClick={onClose}>
139 <span className="material-symbols-outlined">close</span>
140 </button>
141 </div>
142
143 <div className="modal-body">
144 {/* Domain Information */}
145 <section>
146 <h3>Domain Information</h3>
147 <div className="info-grid">
148 <div className="info-item">
149 <label>Domain:</label>
150 <strong style={{ fontSize: '1.1em' }}>{request.domain_name}</strong>
151 </div>
152 <div className="info-item">
153 <label>Registration Period:</label>
154 <span>{request.years} year{request.years !== 1 ? 's' : ''}</span>
155 </div>
156 <div className="info-item">
157 <label>Price:</label>
158 <span>
159 {request.price ? `$${request.price} ${request.currency}` : 'Not specified'}
160 </span>
161 </div>
162 <div className="info-item">
163 <label>Customer:</label>
164 <span>{request.customer_name || '-'}</span>
165 </div>
166 <div className="info-item">
167 <label>Contract:</label>
168 <span>{request.contract_title || 'No contract'}</span>
169 </div>
170 <div className="info-item">
171 <label>Tenant:</label>
172 <span>{request.tenant_subdomain || '-'}</span>
173 </div>
174 </div>
175
176 {request.nameservers && request.nameservers.length > 0 && (
177 <div className="info-item" style={{ marginTop: '16px' }}>
178 <label>Nameservers:</label>
179 <ul style={{ marginTop: '4px', paddingLeft: '20px' }}>
180 {request.nameservers.map((ns, idx) => (
181 <li key={idx}>{ns}</li>
182 ))}
183 </ul>
184 </div>
185 )}
186 </section>
187
188 {/* Request Details */}
189 <section>
190 <h3>Request Details</h3>
191 <div className="info-grid">
192 <div className="info-item">
193 <label>Requested By:</label>
194 <span>{request.requested_by_name || 'Unknown'}</span>
195 </div>
196 <div className="info-item">
197 <label>Requested At:</label>
198 <span>{formatDate(request.requested_at)}</span>
199 </div>
200 {request.reviewed_by_name && (
201 <>
202 <div className="info-item">
203 <label>Reviewed By:</label>
204 <span>{request.reviewed_by_name}</span>
205 </div>
206 <div className="info-item">
207 <label>Reviewed At:</label>
208 <span>{formatDate(request.reviewed_at)}</span>
209 </div>
210 </>
211 )}
212 </div>
213 </section>
214
215 {/* Contact Information */}
216 <section>
217 <h3>Registrant Contact</h3>
218 {formatContact(request.registrant_contact)}
219 </section>
220
221 {/* Review Notes */}
222 {request.approval_notes && (
223 <section>
224 <h3>Review Notes</h3>
225 <div style={{
226 padding: '12px',
227 background: 'var(--surface)',
228 border: '1px solid var(--border)',
229 borderRadius: '4px',
230 whiteSpace: 'pre-wrap'
231 }}>
232 {request.approval_notes}
233 </div>
234 </section>
235 )}
236
237 {/* Registration Error */}
238 {request.registration_error && (
239 <section>
240 <h3>Registration Error</h3>
241 <div style={{
242 padding: '12px',
243 background: '#ffebee',
244 border: '1px solid #ef5350',
245 borderRadius: '4px',
246 color: '#c62828',
247 whiteSpace: 'pre-wrap'
248 }}>
249 {request.registration_error}
250 </div>
251 </section>
252 )}
253
254 {/* Approval/Denial Section (only for pending requests) */}
255 {isPending && (
256 <section>
257 <h3>Admin Action</h3>
258
259 <div className="form-group">
260 <label>Registrar:</label>
261 <select
262 value={selectedRegistrar}
263 onChange={(e) => setSelectedRegistrar(e.target.value)}
264 style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}
265 >
266 <option value="cloudflare-registrar">Cloudflare Registrar (Full domain registration at cost)</option>
267 <option value="cloudflare">Cloudflare (DNS-only zone, no registration)</option>
268 <option value="enom">eNom (Full domain registration + DNS)</option>
269 <option value="moniker">Moniker (Full domain registration + DNS)</option>
270 </select>
271 <small style={{ display: 'block', marginTop: '6px', color: 'var(--text-secondary)' }}>
272 {selectedRegistrar === 'cloudflare-registrar' && '✓ Registers domain at cost price via Cloudflare. No markup, transparent pricing.'}
273 {selectedRegistrar === 'cloudflare' && '✓ Creates DNS zone only. Customer must already own domain or will register elsewhere.'}
274 {selectedRegistrar === 'enom' && '⚠️ Registers domain at eNom and sets up DNS. Requires eNom API credentials.'}
275 {selectedRegistrar === 'moniker' && '⚠️ Registers domain at Moniker and sets up DNS. Requires Moniker API credentials.'}
276 </small>
277 </div>
278
279 <div className="form-group">
280 <label>Notes (optional for approval, required for denial):</label>
281 <textarea
282 value={approvalNotes}
283 onChange={(e) => setApprovalNotes(e.target.value)}
284 placeholder="Add notes about this request (e.g., 'Verified customer identity', 'Insufficient funds', etc.)"
285 rows="4"
286 style={{ width: '100%' }}
287 />
288 </div>
289
290 <div style={{
291 padding: '12px',
292 background: selectedRegistrar === 'cloudflare' ? '#e3f2fd' : (selectedRegistrar === 'cloudflare-registrar' ? '#e8f5e9' : '#fff3e0'),
293 border: `1px solid ${selectedRegistrar === 'cloudflare' ? '#2196f3' : (selectedRegistrar === 'cloudflare-registrar' ? '#4caf50' : '#ff9800')}`,
294 borderRadius: '4px',
295 marginTop: '12px'
296 }}>
297 <strong>{selectedRegistrar === 'cloudflare' || selectedRegistrar === 'cloudflare-registrar' ? 'ℹ️' : '⚠️'} Important:</strong>
298 <ul style={{ marginTop: '8px', paddingLeft: '20px', marginBottom: 0 }}>
299 {selectedRegistrar === 'cloudflare-registrar' && (
300 <>
301 <li>Registers domain via Cloudflare Registrar at cost (no markup)</li>
302 <li>Transparent pricing - you pay what Cloudflare pays</li>
303 <li>Automatic DNS zone creation included</li>
304 <li>Registration cannot be undone if successful</li>
305 </>
306 )}
307 {selectedRegistrar === 'cloudflare' && (
308 <>
309 <li>Creates a free DNS zone in Cloudflare - no domain registration</li>
310 <li>Customer must update nameservers at their registrar</li>
311 <li>Domain will not resolve until nameservers are updated</li>
312 </>
313 )}
314 {(selectedRegistrar === 'enom' || selectedRegistrar === 'moniker') && (
315 <>
316 <li>Approval will immediately attempt to register the domain via {selectedRegistrar === 'enom' ? 'eNom' : 'Moniker'} API</li>
317 <li>Ensure sufficient credits are available in the {selectedRegistrar === 'enom' ? 'eNom' : 'Moniker'} account</li>
318 <li>Registration cannot be undone if successful</li>
319 </>
320 )}
321 </ul>
322 </div>
323 </section>
324 )}
325 </div>
326
327 <div className="modal-footer">
328 {isPending ? (
329 <>
330 <button
331 type="button"
332 className="btn btn-secondary"
333 onClick={onClose}
334 disabled={processing}
335 >
336 Close
337 </button>
338 <div style={{ display: 'flex', gap: '8px' }}>
339 <button
340 type="button"
341 className="btn"
342 onClick={handleDeny}
343 disabled={processing}
344 style={{ background: '#ef5350', color: 'white' }}
345 >
346 {processing ? 'Processing...' : 'Deny Request'}
347 </button>
348 <button
349 type="button"
350 className="btn btn-primary"
351 onClick={handleApprove}
352 disabled={processing}
353 >
354 {processing ? 'Processing...' : 'Approve & Register'}
355 </button>
356 </div>
357 </>
358 ) : (
359 <button
360 type="button"
361 className="btn btn-primary"
362 onClick={onClose}
363 >
364 Close
365 </button>
366 )}
367 </div>
368 </div>
369 </div>
370 );
371}