EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabWordPress.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { apiFetch } from '../../lib/api';
3
4export default function TabWordPress() {
5 const [sites, setSites] = useState([]);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState('');
8 const [showCreateForm, setShowCreateForm] = useState(false);
9 const [creating, setCreating] = useState(false);
10 const [customers, setCustomers] = useState([]);
11
12 // Form state
13 const [formData, setFormData] = useState({
14 siteName: '',
15 customerId: '',
16 domain: '',
17 dbPassword: '',
18 spacesKey: '',
19 spacesSecret: ''
20 });
21
22 useEffect(() => {
23 fetchSites();
24 fetchCustomers();
25 }, []);
26
27 const fetchSites = async () => {
28 setLoading(true);
29 setError('');
30 try {
31 const res = await apiFetch('/wordpress/sites');
32 if (res.ok) {
33 const data = await res.json();
34 setSites(data.sites || []);
35 } else {
36 const errorData = await res.json();
37 setError(errorData.error || 'Failed to fetch WordPress sites');
38 }
39 } catch (err) {
40 console.error('Error fetching sites:', err);
41 setError(err.message || 'Failed to load sites');
42 } finally {
43 setLoading(false);
44 }
45 };
46
47 const fetchCustomers = async () => {
48 try {
49 const res = await apiFetch('/customers');
50 if (res.ok) {
51 const data = await res.json();
52 setCustomers(data.customers || []);
53 }
54 } catch (err) {
55 console.error('Error fetching customers:', err);
56 }
57 };
58
59 const handleInputChange = (e) => {
60 const { name, value } = e.target;
61 setFormData(prev => ({
62 ...prev,
63 [name]: value
64 }));
65 };
66
67 const handleSubmit = async (e) => {
68 e.preventDefault();
69 setCreating(true);
70 setError('');
71
72 try {
73 const res = await apiFetch('/wordpress/create', {
74 method: 'POST',
75 headers: { 'Content-Type': 'application/json' },
76 body: JSON.stringify(formData)
77 });
78
79 if (res.ok) {
80 const data = await res.json();
81 alert(`WordPress site created successfully!\nApp ID: ${data.appId}\nURL: ${data.url || 'Deploying...'}`);
82 setShowCreateForm(false);
83 setFormData({
84 siteName: '',
85 customerId: '',
86 domain: '',
87 dbPassword: '',
88 spacesKey: '',
89 spacesSecret: ''
90 });
91 fetchSites();
92 } else {
93 const errorData = await res.json();
94 setError(errorData.error || 'Failed to create WordPress site');
95 }
96 } catch (err) {
97 console.error('Error creating site:', err);
98 setError(err.message || 'Failed to create WordPress site');
99 } finally {
100 setCreating(false);
101 }
102 };
103
104 const getStatusColor = (status) => {
105 const statusMap = {
106 'running': 'success',
107 'deploying': 'info',
108 'building': 'info',
109 'error': 'error',
110 'stopped': 'secondary'
111 };
112 return statusMap[status?.toLowerCase()] || 'secondary';
113 };
114
115 const formatTime = (date) => {
116 if (!date) return '';
117 return new Date(date).toLocaleString();
118 };
119
120 if (loading) {
121 return (
122 <div style={{ padding: '40px', textAlign: 'center' }}>
123 <div className="spinner"></div>
124 </div>
125 );
126 }
127
128 return (
129 <div style={{ padding: '20px' }}>
130 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
131 <div>
132 <h2>WordPress Sites</h2>
133 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
134 Create and manage WordPress sites on DigitalOcean
135 </p>
136 </div>
137 <button
138 onClick={() => setShowCreateForm(!showCreateForm)}
139 className="btn btn-primary"
140 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
141 >
142 <span className="material-symbols-outlined">
143 {showCreateForm ? 'close' : 'add'}
144 </span>
145 {showCreateForm ? 'Cancel' : 'Create WordPress Site'}
146 </button>
147 </div>
148
149 {error && (
150 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
151 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
152 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>
153 error
154 </span>
155 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
156 </div>
157 </div>
158 )}
159
160 {/* Create Form */}
161 {showCreateForm && (
162 <div className="card" style={{ padding: '24px', marginBottom: '20px' }}>
163 <h3 style={{ marginBottom: '20px' }}>Create New WordPress Site</h3>
164 <form onSubmit={handleSubmit}>
165 <div style={{ display: 'grid', gap: '16px', gridTemplateColumns: '1fr 1fr' }}>
166 <div>
167 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
168 Site Name *
169 </label>
170 <input
171 type="text"
172 name="siteName"
173 value={formData.siteName}
174 onChange={handleInputChange}
175 required
176 placeholder="my-awesome-site"
177 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
178 />
179 <small style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>
180 Lowercase letters, numbers, and hyphens only
181 </small>
182 </div>
183
184 <div>
185 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
186 Customer
187 </label>
188 <select
189 name="customerId"
190 value={formData.customerId}
191 onChange={handleInputChange}
192 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
193 >
194 <option value="">Select customer (optional)</option>
195 {customers.map(customer => (
196 <option key={customer.customer_id} value={customer.customer_id}>
197 {customer.name}
198 </option>
199 ))}
200 </select>
201 </div>
202
203 <div>
204 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
205 Domain (Optional)
206 </label>
207 <input
208 type="text"
209 name="domain"
210 value={formData.domain}
211 onChange={handleInputChange}
212 placeholder="example.com"
213 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
214 />
215 <small style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>
216 Leave empty to use DO App Platform domain
217 </small>
218 </div>
219
220 <div>
221 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
222 Database Password *
223 </label>
224 <input
225 type="password"
226 name="dbPassword"
227 value={formData.dbPassword}
228 onChange={handleInputChange}
229 required
230 placeholder="Strong password"
231 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
232 />
233 </div>
234
235 <div>
236 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
237 DO Spaces Access Key *
238 </label>
239 <input
240 type="text"
241 name="spacesKey"
242 value={formData.spacesKey}
243 onChange={handleInputChange}
244 required
245 placeholder="DO00..."
246 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
247 />
248 </div>
249
250 <div>
251 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
252 DO Spaces Secret Key *
253 </label>
254 <input
255 type="password"
256 name="spacesSecret"
257 value={formData.spacesSecret}
258 onChange={handleInputChange}
259 required
260 placeholder="Secret key"
261 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
262 />
263 </div>
264 </div>
265
266 <div style={{ marginTop: '24px', display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
267 <button
268 type="button"
269 onClick={() => setShowCreateForm(false)}
270 className="btn"
271 >
272 Cancel
273 </button>
274 <button
275 type="submit"
276 disabled={creating}
277 className="btn btn-primary"
278 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
279 >
280 {creating ? (
281 <>
282 <span className="spinner" style={{ width: '16px', height: '16px' }}></span>
283 Creating...
284 </>
285 ) : (
286 <>
287 <span className="material-symbols-outlined">rocket_launch</span>
288 Create WordPress Site
289 </>
290 )}
291 </button>
292 </div>
293 </form>
294 </div>
295 )}
296
297 {/* Sites List */}
298 {sites.length === 0 && !showCreateForm ? (
299 <div className="card" style={{ padding: '60px 20px', textAlign: 'center' }}>
300 <span className="material-symbols-outlined" style={{ fontSize: '64px', marginBottom: '16px', display: 'block', opacity: 0.5, color: 'var(--text-secondary)' }}>
301 web
302 </span>
303 <p style={{ color: 'var(--text-secondary)' }}>No WordPress sites found</p>
304 <p style={{ fontSize: '14px', color: 'var(--text-tertiary)', marginTop: '8px' }}>
305 Create your first WordPress site to get started
306 </p>
307 </div>
308 ) : (
309 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
310 {sites.map((site) => (
311 <div key={site.app_id || site.id} className="card">
312 <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
313 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
314 <div>
315 <h3 style={{ margin: 0, marginBottom: '4px' }}>{site.name}</h3>
316 {site.customer_name && (
317 <p style={{ fontSize: '13px', color: 'var(--text-secondary)', margin: 0 }}>
318 {site.customer_name}
319 </p>
320 )}
321 </div>
322 <span className={`badge badge-${getStatusColor(site.status)}`}>
323 {site.status || 'unknown'}
324 </span>
325 </div>
326
327 {site.url && (
328 <a
329 href={site.url}
330 target="_blank"
331 rel="noopener noreferrer"
332 style={{
333 fontSize: '13px',
334 color: 'var(--primary)',
335 textDecoration: 'none',
336 display: 'flex',
337 alignItems: 'center',
338 gap: '4px'
339 }}
340 >
341 <span>{site.url}</span>
342 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
343 open_in_new
344 </span>
345 </a>
346 )}
347 </div>
348
349 <div style={{ padding: '16px' }}>
350 <div style={{ display: 'grid', gap: '8px', fontSize: '13px' }}>
351 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
352 <span style={{ color: 'var(--text-secondary)' }}>Region:</span>
353 <span>{site.region || 'syd1'}</span>
354 </div>
355 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
356 <span style={{ color: 'var(--text-secondary)' }}>Created:</span>
357 <span>{formatTime(site.created_at)}</span>
358 </div>
359 {site.app_id && (
360 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
361 <span style={{ color: 'var(--text-secondary)' }}>App ID:</span>
362 <span style={{ fontFamily: 'monospace', fontSize: '11px' }}>
363 {site.app_id}
364 </span>
365 </div>
366 )}
367 </div>
368
369 <div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
370 <button
371 className="btn btn-sm"
372 onClick={() => window.open(`https://cloud.digitalocean.com/apps/${site.app_id}`, '_blank')}
373 disabled={!site.app_id}
374 style={{ flex: 1, fontSize: '12px' }}
375 >
376 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
377 settings
378 </span>
379 Manage
380 </button>
381 <button
382 className="btn btn-sm"
383 onClick={() => fetchSites()}
384 style={{ fontSize: '12px' }}
385 >
386 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
387 refresh
388 </span>
389 </button>
390 </div>
391 </div>
392 </div>
393 ))}
394 </div>
395 )}
396 </div>
397 );
398}