EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabAppServices.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
4
5export default function TabAppServices() {
6 const navigate = useNavigate();
7 const [apps, setApps] = useState([]);
8 const [loading, setLoading] = useState(true);
9 const [error, setError] = useState('');
10 const [syncing, setSyncing] = useState(false);
11 const [lastSync, setLastSync] = useState(null);
12
13 useEffect(() => {
14 fetchApps();
15 }, []);
16
17 const fetchApps = async () => {
18 setLoading(true);
19 setError('');
20 try {
21 const res = await apiFetch('/hosting/apps');
22 if (res.ok) {
23 const data = await res.json();
24 setApps(data.apps || []);
25 setLastSync(data.lastSync);
26 } else {
27 const errorData = await res.json();
28 setError(errorData.error || 'Failed to fetch apps');
29 }
30 } catch (err) {
31 console.error('Error fetching apps:', err);
32 setError(err.message || 'Failed to load apps');
33 } finally {
34 setLoading(false);
35 }
36 };
37
38 const handleSync = async () => {
39 setSyncing(true);
40 setError('');
41 try {
42 const res = await apiFetch('/hosting/sync', { method: 'POST' });
43 if (res.ok) {
44 await fetchApps();
45 } else {
46 const errorData = await res.json();
47 setError(errorData.error || 'Failed to sync');
48 }
49 } catch (err) {
50 console.error('Error syncing apps:', err);
51 setError(err.message || 'Failed to sync apps');
52 } finally {
53 setSyncing(false);
54 }
55 };
56
57 const getStatusColor = (status) => {
58 const statusMap = {
59 'running': 'success',
60 'active': 'success',
61 'building': 'info',
62 'deploying': 'info',
63 'error': 'danger',
64 'canceled': 'warning',
65 'deleted': 'error',
66 'terminated': 'error'
67 };
68 return statusMap[status?.toLowerCase()] || 'secondary';
69 };
70
71 const getStatusLabel = (status) => {
72 if (status?.toLowerCase() === 'deleted') return 'terminated';
73 return status;
74 };
75
76 const formatTime = (date) => {
77 if (!date) return '';
78 const now = new Date();
79 const then = new Date(date);
80 const diff = Math.floor((now - then) / 1000);
81
82 if (diff < 60) return `${diff} seconds ago`;
83 if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
84 if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
85 return `${Math.floor(diff / 86400)} days ago`;
86 };
87
88 if (loading) {
89 return (
90 <div style={{ padding: '40px', textAlign: 'center' }}>
91 <div className="spinner"></div>
92 </div>
93 );
94 }
95
96 return (
97 <div style={{ padding: '20px' }}>
98 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
99 <div>
100 <h2>App Services</h2>
101 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
102 Manage your DigitalOcean App Platform applications (Node.js, Static Sites)
103 </p>
104 </div>
105 <button
106 onClick={handleSync}
107 disabled={syncing}
108 className="btn btn-primary"
109 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
110 >
111 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
112 refresh
113 </span>
114 {syncing ? 'Syncing...' : 'Sync Now'}
115 </button>
116 </div>
117
118 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--info-bg)', border: '1px solid var(--info-border)' }}>
119 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
120 <span className="material-symbols-outlined" style={{ color: 'var(--info-text)' }}>
121 schedule
122 </span>
123 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
124 Data refreshes every 5 minutes from DigitalOcean.
125 {lastSync && (
126 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
127 Last synced {formatTime(lastSync)}
128 </span>
129 )}
130 </div>
131 </div>
132 </div>
133
134 {error && (
135 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
136 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
137 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>
138 error
139 </span>
140 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
141 </div>
142 </div>
143 )}
144
145 {apps.length === 0 ? (
146 <div className="card" style={{ padding: '60px 20px', textAlign: 'center' }}>
147 <span className="material-symbols-outlined" style={{ fontSize: '64px', marginBottom: '16px', display: 'block', opacity: 0.5, color: 'var(--text-secondary)' }}>
148 cloud
149 </span>
150 <p style={{ color: 'var(--text-secondary)' }}>No apps found</p>
151 <p style={{ fontSize: '14px', color: 'var(--text-tertiary)', marginTop: '8px' }}>
152 Deploy an app in your DigitalOcean App Platform
153 </p>
154 </div>
155 ) : (
156 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
157 {apps.map((app) => (
158 <div
159 key={app.do_app_id}
160 className="card"
161 style={{ overflow: 'hidden', cursor: 'pointer' }}
162 onClick={() => navigate(`/hosting/apps/${app.app_id}`)}
163 >
164 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
165 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
166 <div style={{ flex: 1 }}>
167 <h3 style={{ fontSize: '18px', marginBottom: '6px', fontWeight: '600' }}>{app.app_name}</h3>
168 <p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
169 {app.customer_name || 'Unassigned'}
170 </p>
171 </div>
172 <span className={`badge badge-${getStatusColor(app.status)}`} style={{ textTransform: 'capitalize' }}>
173 {getStatusLabel(app.status)}
174 </span>
175 </div>
176
177 {app.live_url && (
178 <a
179 href={app.live_url}
180 target="_blank"
181 rel="noopener noreferrer"
182 style={{
183 fontSize: '13px',
184 color: 'var(--primary)',
185 textDecoration: 'none',
186 display: 'flex',
187 alignItems: 'center',
188 gap: '4px'
189 }}
190 >
191 <span>{app.live_url}</span>
192 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>open_in_new</span>
193 </a>
194 )}
195 </div>
196
197 <div style={{ padding: '20px', background: 'var(--surface-2, #f8f9fa)' }}>
198 <dl style={{ display: 'grid', gap: '14px', fontSize: '14px', margin: 0 }}>
199 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
200 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Type</dt>
201 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{app.app_type || 'nodejs'}</dd>
202 </div>
203 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
204 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Region</dt>
205 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{app.region || 'N/A'}</dd>
206 </div>
207 {app.metadata?.tier_slug && (
208 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
209 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Tier</dt>
210 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)', textTransform: 'capitalize' }}>
211 {app.metadata.tier_slug}
212 </dd>
213 </div>
214 )}
215 </dl>
216 </div>
217 </div>
218 ))}
219 </div>
220 )}
221 </div>
222 );
223}