feat(frontend): show active app domain with click-to-copy
Surface the domain the app is actually reachable on right now (verified custom domain, else the platform subdomain) as a single "Application Domain" field. Clicking it copies the address to the clipboard and shows a "Copied" toast. Centralizes the active-domain logic so the page header and the verified-custom-domain block stay in sync, and always loads domain-info so the platform host is correct even without a custom domain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -303,7 +303,9 @@ export default function AppDetailPage() {
|
||||
}>({
|
||||
queryKey: ['domain-info', appId],
|
||||
queryFn: () => api.get(`/applications/${appId}/domain`).then((r) => r.data),
|
||||
enabled: showDomainSetup || (!!app && (app.customDomainStatus === 'pending_dns' || app.customDomainStatus === 'verified')),
|
||||
// Always load: we surface the active app domain (platform host included) even
|
||||
// when no custom domain is configured, so the platform host must be available.
|
||||
enabled: !!app,
|
||||
});
|
||||
|
||||
const { data: domainPriceData } = useQuery<{ monthlyPrice: number }>({
|
||||
@@ -1045,6 +1047,18 @@ export default function AppDetailPage() {
|
||||
|
||||
const pageLocked = isAnyDeleting;
|
||||
|
||||
// The host the app is actually reachable on right now: the verified custom
|
||||
// domain when present, otherwise the platform-assigned subdomain. Single source
|
||||
// of truth so every place that shows "the app's domain" stays in sync.
|
||||
const platformHost = `${app.subdomain}.${domainInfo?.platformDomain || 'apps.cloudhost.ir'}`;
|
||||
const currentDomain =
|
||||
app.customDomain && app.customDomainStatus === 'verified' ? app.customDomain : platformHost;
|
||||
|
||||
const copyDomain = (domain: string) => {
|
||||
navigator.clipboard.writeText(domain);
|
||||
notify.success(ad.copied);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeletingModal open={pageLocked} resourceName={app.name} resourceKind="application" />
|
||||
@@ -1067,10 +1081,7 @@ export default function AppDetailPage() {
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{app.runtime}{app.runtimeVersion ? ` v${app.runtimeVersion}` : ''}{app.phpVersion ? ` — PHP ${app.phpVersion}` : ''} ·{' '}
|
||||
{app.customDomain && app.customDomainStatus === 'verified'
|
||||
? <a href={`https://${app.customDomain}`} target="_blank" rel="noopener noreferrer" className="text-emerald-600 hover:underline">{app.customDomain}</a>
|
||||
: <span>{app.subdomain}.{domainInfo?.platformDomain || 'apps.cloudhost.ir'}</span>
|
||||
}
|
||||
<a href={`https://${currentDomain}`} target="_blank" rel="noopener noreferrer" dir="ltr" className="text-primary-600 hover:underline">{currentDomain}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1604,16 +1615,24 @@ export default function AppDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform domain (always shown) */}
|
||||
{/* Application domain — the host the app is reachable on right now
|
||||
(verified custom domain, else the platform subdomain). Click to copy. */}
|
||||
<div className="bg-gray-50 rounded-xl p-4 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">{ad.platformDomain}</p>
|
||||
<p className="text-sm font-mono font-medium text-gray-800">
|
||||
{app.subdomain}.{domainInfo?.platformDomain || 'apps.cloudhost.ir'}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-gray-500 mb-1">{ad.appDomain}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyDomain(currentDomain)}
|
||||
title={ad.copyDomainHint}
|
||||
dir="ltr"
|
||||
className="group inline-flex items-center gap-1.5 max-w-full text-sm font-mono font-medium text-gray-800 hover:text-primary-600 transition-colors"
|
||||
>
|
||||
<span className="truncate">{currentDomain}</span>
|
||||
<Copy className="w-3.5 h-3.5 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="badge badge-green text-xs">{ad.active}</span>
|
||||
<span className="badge badge-green text-xs shrink-0">{ad.active}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1623,7 +1642,16 @@ export default function AppDetailPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-emerald-600 mb-1">{ad.customDomain}</p>
|
||||
<p className="text-sm font-mono font-medium text-emerald-800">{app.customDomain}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyDomain(app.customDomain!)}
|
||||
title={ad.copyDomainHint}
|
||||
dir="ltr"
|
||||
className="group inline-flex items-center gap-1.5 max-w-full text-sm font-mono font-medium text-emerald-800 hover:text-emerald-600 transition-colors"
|
||||
>
|
||||
<span className="truncate">{app.customDomain}</span>
|
||||
<Copy className="w-3.5 h-3.5 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
<p className="text-xs text-emerald-500 mt-1">
|
||||
<CheckCircle className="w-3 h-3 inline" /> SSL Active — Verified on{' '}
|
||||
{app.customDomainVerifiedAt ? new Date(app.customDomainVerifiedAt).toLocaleString(locale) : ''}
|
||||
|
||||
Reference in New Issue
Block a user