1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
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);
17 const fetchApps = async () => {
21 const res = await apiFetch('/hosting/apps');
23 const data = await res.json();
24 setApps(data.apps || []);
25 setLastSync(data.lastSync);
27 const errorData = await res.json();
28 setError(errorData.error || 'Failed to fetch apps');
31 console.error('Error fetching apps:', err);
32 setError(err.message || 'Failed to load apps');
38 const handleSync = async () => {
42 const res = await apiFetch('/hosting/sync', { method: 'POST' });
46 const errorData = await res.json();
47 setError(errorData.error || 'Failed to sync');
50 console.error('Error syncing apps:', err);
51 setError(err.message || 'Failed to sync apps');
57 const getStatusColor = (status) => {
64 'canceled': 'warning',
68 return statusMap[status?.toLowerCase()] || 'secondary';
71 const getStatusLabel = (status) => {
72 if (status?.toLowerCase() === 'deleted') return 'terminated';
76 const formatTime = (date) => {
78 const now = new Date();
79 const then = new Date(date);
80 const diff = Math.floor((now - then) / 1000);
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`;
90 <div style={{ padding: '40px', textAlign: 'center' }}>
91 <div className="spinner"></div>
97 <div style={{ padding: '20px' }}>
98 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
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)
108 className="btn btn-primary"
109 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
111 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
114 {syncing ? 'Syncing...' : 'Sync Now'}
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)' }}>
123 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
124 Data refreshes every 5 minutes from DigitalOcean.
126 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
127 Last synced {formatTime(lastSync)}
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)' }}>
140 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
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)' }}>
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
156 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
161 style={{ overflow: 'hidden', cursor: 'pointer' }}
162 onClick={() => navigate(`/hosting/apps/${app.app_id}`)}
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'}
172 <span className={`badge badge-${getStatusColor(app.status)}`} style={{ textTransform: 'capitalize' }}>
173 {getStatusLabel(app.status)}
181 rel="noopener noreferrer"
184 color: 'var(--primary)',
185 textDecoration: 'none',
187 alignItems: 'center',
191 <span>{app.live_url}</span>
192 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>open_in_new</span>
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>
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>
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}