- Deleted unused/stub routes: Security, Analytics, Marketing (+SEO/Social), Integrations - Removed these routes from the Dashboard Sidebar menu - Fixed Next.js build errors caused by duplicate component declarations (SectionHeader, KvRow) in overview, hosting, services, and infrastructure by relying fully on the unified dashboard-ui kit.
190 lines
5.0 KiB
TypeScript
190 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { useParams } from "next/navigation";
|
|
import { useState } from "react";
|
|
import { Globe, ExternalLink, Copy, Check, Loader2 } from "lucide-react";
|
|
import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
|
|
import {
|
|
THEME,
|
|
PageHeader,
|
|
Card,
|
|
EmptyState,
|
|
} from "@/components/project/dashboard-ui";
|
|
|
|
type LiveApp = Anatomy["hosting"]["live"][number];
|
|
|
|
// All public URLs for an app: its fqdn(s) (Coolify can store a comma-joined
|
|
// list) plus any attached custom domains, de-duplicated.
|
|
function urlsFor(app: LiveApp): string[] {
|
|
const out = new Set<string>();
|
|
(app.fqdn ?? "")
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.forEach((u) => out.add(u));
|
|
(app.domains ?? []).forEach((d) =>
|
|
out.add(d.startsWith("http") ? d : `https://${d}`),
|
|
);
|
|
return [...out];
|
|
}
|
|
|
|
export default function DomainsPage() {
|
|
const params = useParams();
|
|
const projectId = params.projectId as string;
|
|
const { anatomy, loading } = useAnatomy(projectId, { pollMs: 8000 });
|
|
const live = anatomy?.hosting.live ?? [];
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: "100vh",
|
|
background: THEME.canvasGradient,
|
|
fontFamily: THEME.font,
|
|
padding: "36px 48px",
|
|
}}
|
|
>
|
|
<div style={{ maxWidth: 860, margin: "0 auto" }}>
|
|
<PageHeader
|
|
title="Domains"
|
|
subtitle="Public URLs for your deployed apps. To add a custom domain, ask the AI in chat — DNS + TLS are wired automatically."
|
|
/>
|
|
|
|
{loading && !anatomy ? (
|
|
<Card>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
color: THEME.mid,
|
|
fontSize: "0.875rem",
|
|
}}
|
|
>
|
|
<Loader2 size={15} className="animate-spin" /> Loading…
|
|
</div>
|
|
</Card>
|
|
) : live.length === 0 ? (
|
|
<EmptyState
|
|
icon={<Globe size={22} />}
|
|
title="No deployed apps yet"
|
|
hint="Once you deploy an app, its URL and any custom domains will appear here."
|
|
/>
|
|
) : (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
{live.map((app) => (
|
|
<DomainCard key={app.uuid} app={app} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DomainCard({ app }: { app: LiveApp }) {
|
|
const urls = urlsFor(app);
|
|
return (
|
|
<Card padding={0}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "14px 20px",
|
|
borderBottom: urls.length ? `1px solid ${THEME.borderSoft}` : "none",
|
|
}}
|
|
>
|
|
<span
|
|
style={{ fontSize: "0.95rem", fontWeight: 600, color: THEME.ink }}
|
|
>
|
|
{app.name}
|
|
</span>
|
|
<span style={{ fontSize: "0.78rem", color: THEME.muted }}>
|
|
{app.sourceLabel}
|
|
</span>
|
|
</div>
|
|
|
|
{urls.length === 0 ? (
|
|
<div
|
|
style={{
|
|
padding: "14px 20px",
|
|
fontSize: "0.85rem",
|
|
color: THEME.mid,
|
|
fontStyle: "italic",
|
|
}}
|
|
>
|
|
No domain assigned yet — still deploying.
|
|
</div>
|
|
) : (
|
|
urls.map((url, i) => (
|
|
<DomainRow key={url} url={url} last={i === urls.length - 1} />
|
|
))
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function DomainRow({ url, last }: { url: string; last: boolean }) {
|
|
const [copied, setCopied] = useState(false);
|
|
const copy = () => {
|
|
navigator.clipboard?.writeText(url).then(() => {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1500);
|
|
});
|
|
};
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
padding: "12px 20px",
|
|
borderBottom: last ? "none" : `1px solid ${THEME.borderSoft}`,
|
|
}}
|
|
>
|
|
<Globe size={14} style={{ color: THEME.muted, flexShrink: 0 }} />
|
|
<a
|
|
href={url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
style={{
|
|
fontSize: "0.85rem",
|
|
color: THEME.ink,
|
|
textDecoration: "none",
|
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
flex: 1,
|
|
}}
|
|
>
|
|
{url.replace(/^https?:\/\//, "")}
|
|
</a>
|
|
<a
|
|
href={url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
title="Open"
|
|
style={{ color: THEME.muted, display: "inline-flex", flexShrink: 0 }}
|
|
>
|
|
<ExternalLink size={14} />
|
|
</a>
|
|
<button
|
|
onClick={copy}
|
|
title="Copy URL"
|
|
style={{
|
|
background: "transparent",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
color: copied ? "#16a34a" : THEME.muted,
|
|
display: "inline-flex",
|
|
padding: 0,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|