TorbatYar/frontend/modules/commercial/features/domains.tsx

178 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { AuthGuard } from "@/components/AuthGuard";
import { Button, Container } from "@/components/ui";
import { api, ApiError } from "@/lib/api";
import { useMe } from "@/hooks/useMe";
import { evaluateDomainPolicy, loadWorkspaceSnapshot } from "../adapters";
import type { AdapterResult, CommercialWorkspaceSnapshot, DomainPolicyResult } from "../types";
import { DomainPolicyPanel, FeatureLock, PlatformShell } from "../components";
import { CommercialEmptyState } from "../components/CommercialEmptyState";
import { friendlyMessage } from "../presentation";
import { CommercialSkeleton } from "../components/CommercialSkeleton";
function DomainsInner() {
const { me, loading: meLoading } = useMe();
const [snap, setSnap] = useState<AdapterResult<CommercialWorkspaceSnapshot | null> | null>(null);
const [policy, setPolicy] = useState<DomainPolicyResult | null>(null);
const [customDomain, setCustomDomain] = useState("");
const [error, setError] = useState("");
const [msg, setMsg] = useState("");
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const s = await loadWorkspaceSnapshot();
setSnap(s);
if (s.data) {
setPolicy(s.data.domain_policy || null);
} else if (me?.current_tenant_id) {
const pol = await evaluateDomainPolicy({ tenant_id: me.current_tenant_id });
setPolicy(pol.data);
}
}, [me?.current_tenant_id]);
useEffect(() => {
if (meLoading || !me) return;
void load();
}, [me, meLoading, load]);
const unlocked = Boolean(snap?.data?.custom_domain_unlocked ?? policy?.custom_domain_allowed);
const submit = async () => {
if (!snap?.data?.tenant_id || !customDomain.trim()) return;
setBusy(true);
setError("");
setMsg("");
try {
if (!unlocked) {
setError(friendlyMessage(policy?.reason, "برای اتصال دامنه اختصاصی، ابتدا اشتراک خود را فعال کنید."));
setBusy(false);
return;
}
await api.onboarding.updateDomain(snap.data.tenant_id, {
custom_domain: customDomain.trim(),
});
setMsg("دامنه ثبت شد. مراحل تأیید DNS را در همین صفحه دنبال کنید.");
await load();
} catch (err) {
setError(friendlyMessage(err instanceof ApiError ? err.message : null, "ثبت دامنه انجام نشد. آدرس را بررسی و دوباره تلاش کنید."));
} finally {
setBusy(false);
}
};
if (meLoading) {
return (
<Container className="py-16 text-center">
<CommercialSkeleton rows={4} className="mx-auto max-w-3xl" />
</Container>
);
}
const ws = snap?.data;
return (
<PlatformShell title="دامنه و امنیت" subtitle="در دوره آزمایشی زیردامنه رایگان دارید؛ دامنه اختصاصی با اشتراک فعال قابل اتصال است.">
{!ws ? (
<CommercialEmptyState title="فضای کاری شما هنوز ساخته نشده" description="ابتدا راه‌اندازی اولیه را کامل کنید تا زیردامنه رایگان شما فعال شود." actionHref="/onboarding" actionLabel="شروع راه‌اندازی" />
) : (
<div className="space-y-6">
<div className="rounded-2xl border border-gray-100 bg-white p-5 dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center justify-between gap-3"><p className="text-sm text-gray-500">زیردامنه رایگان</p><span className="rounded-full bg-emerald-50 px-2.5 py-1 text-xs text-emerald-700">فعال</span></div>
<p className="mt-1 font-mono text-lg text-secondary dark:text-gray-100" dir="ltr">
{ws.slug}.torbatyar.ir
</p>
{ws.primary_domain ? (
<p className="mt-2 text-sm">
دامنه اصلی:{" "}
<span className="font-mono" dir="ltr">
{ws.primary_domain}
</span>
</p>
) : null}
</div>
<DomainPolicyPanel unlocked={unlocked} policy={policy || ws.domain_policy} tenantId={ws.tenant_id} />
<FeatureLock
locked={!unlocked}
reason={policy?.reason}
tenantId={ws.tenant_id}
featureKey="custom_domain"
fallback={
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-5 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-900">
برای اتصال دامنه اختصاصی، یک اشتراک فعال انتخاب کنید.{" "}
<Link href="/billing" className="font-semibold text-primary">
مشاهده پلنها
</Link>
</div>
}
>
<div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-5 dark:border-gray-800 dark:bg-gray-900">
<label className="block text-sm font-medium text-secondary dark:text-gray-100">
اتصال دامنه اختصاصی
<input
dir="ltr"
className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-950"
placeholder="shop.example.com"
value={customDomain}
onChange={(e) => setCustomDomain(e.target.value)}
/>
</label>
<Button loading={busy} onClick={() => void submit()}>
ذخیره و شروع تأیید
</Button>
</div>
</FeatureLock>
<section>
<h3 className="text-sm font-semibold text-secondary dark:text-gray-100">وضعیت اتصال دامنهها</h3>
{ws.domains.length ? (
<ul className="mt-3 space-y-2">
{ws.domains.map((d) => (
<li
key={d.id}
className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-gray-100 bg-white px-3 py-2 text-sm dark:border-gray-800 dark:bg-gray-900"
>
<span className="font-mono" dir="ltr">
{d.domain}
</span>
<div className="flex flex-wrap gap-2 text-xs">
<span className="rounded-full bg-gray-100 px-2.5 py-1 text-gray-600">{d.domain_type === "custom" ? "دامنه اختصاصی" : "زیردامنه"}</span>
<span className={`rounded-full px-2.5 py-1 ${d.is_verified ? "bg-emerald-50 text-emerald-700" : "bg-amber-50 text-amber-700"}`}>{d.is_verified ? "DNS تأیید شده" : "در انتظار تأیید DNS"}</span>
<span className={`rounded-full px-2.5 py-1 ${d.is_verified ? "bg-sky-50 text-sky-700" : "bg-gray-100 text-gray-600"}`}>{d.is_verified ? "SSL فعال" : "SSL پس از تأیید"}</span>
</div>
</li>
))}
</ul>
) : (
<CommercialEmptyState
title="دامنه‌ای ثبت نشده"
description="زیردامنه فضای کاری به‌صورت خودکار فعال است. با اشتراک فعال می‌توانید دامنه اختصاصی اضافه کنید."
hint="وضعیت تأیید DNS و SSL پس از ثبت دامنه اینجا نمایش داده می‌شود."
actionHref="/billing"
actionLabel="مشاهده پلن‌ها"
secondaryHref="/help"
secondaryLabel="راهنمای دامنه"
/>
)}
</section>
{error ? <p className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p> : null}
{msg ? <p className="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{msg}</p> : null}
</div>
)}
</PlatformShell>
);
}
export function CommercialDomainsPage() {
return (
<AuthGuard>
<DomainsInner />
</AuthGuard>
);
}