Add workshop generate-link type, Email DNS for domains, and editor polish.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
6be867ea7a
commit
3cbadb4001
@@ -5,6 +5,7 @@ import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Globe,
|
||||
Mail,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
UserRound,
|
||||
@@ -20,6 +21,7 @@ type PopoverPosition = {
|
||||
interface DomainLinksOverflowMenuProps {
|
||||
host: string
|
||||
onAiPrompts: () => void
|
||||
onEmailDns: () => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
@@ -42,6 +44,7 @@ function openHost(host: string) {
|
||||
export function DomainLinksOverflowMenu({
|
||||
host,
|
||||
onAiPrompts,
|
||||
onEmailDns,
|
||||
onRemove,
|
||||
}: DomainLinksOverflowMenuProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -61,7 +64,7 @@ export function DomainLinksOverflowMenu({
|
||||
|
||||
const size = popoverRef.current
|
||||
? { width: popoverRef.current.offsetWidth, height: popoverRef.current.offsetHeight }
|
||||
: { width: 220, height: 280 }
|
||||
: { width: 220, height: 320 }
|
||||
|
||||
setPopoverPos(fitFixedPopover(rect, size, 'end'))
|
||||
}
|
||||
@@ -190,6 +193,18 @@ export function DomainLinksOverflowMenu({
|
||||
</button>
|
||||
))}
|
||||
<div className={styles.menuDivider} role="separator" />
|
||||
<button
|
||||
type="button"
|
||||
className={styles.item}
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
onEmailDns()
|
||||
}}
|
||||
>
|
||||
<Mail size={16} />
|
||||
<span>Email DNS</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.item}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
.providerRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.providerLabel {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: 110px;
|
||||
padding: var(--field-padding-y) var(--field-padding-x);
|
||||
font-size: var(--field-font-size);
|
||||
font-family: var(--font-ui);
|
||||
line-height: 1.45;
|
||||
color: var(--text-primary);
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: var(--radius-sm);
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.textarea:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);
|
||||
}
|
||||
|
||||
.textarea:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Modal } from './Modal'
|
||||
import { DnsProviderSwitch } from './DnsProviderSwitch'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { applyMailDns } from '../services/domainService'
|
||||
import type { DomainListItem, ParkedDnsProvider } from '../types/domain'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import tableStyles from '../pages/BusinessesPage.module.css'
|
||||
import parkedStyles from './ParkedDomainsModal.module.css'
|
||||
import styles from './EmailDnsModal.module.css'
|
||||
|
||||
interface EmailDnsModalProps {
|
||||
domain: DomainListItem | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function EmailDnsModal({ domain, onClose }: EmailDnsModalProps) {
|
||||
const { showToast } = useToast()
|
||||
const [dnsProvider, setDnsProvider] = useState<ParkedDnsProvider>('arvan')
|
||||
const [ed25519Key, setEd25519Key] = useState('')
|
||||
const [rsaKey, setRsaKey] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!domain) return
|
||||
setDnsProvider('arvan')
|
||||
setEd25519Key('')
|
||||
setRsaKey('')
|
||||
setError('')
|
||||
setSubmitting(false)
|
||||
}, [domain])
|
||||
|
||||
if (!domain) return null
|
||||
|
||||
async function submit() {
|
||||
setSubmitting(true)
|
||||
setError('')
|
||||
try {
|
||||
const result = await applyMailDns(domain.id, {
|
||||
dnsProvider,
|
||||
ed25519Key,
|
||||
rsaKey,
|
||||
})
|
||||
showToast(result.message, 'success')
|
||||
onClose()
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ApiError ? err.message : 'Unable to update mail DNS.'
|
||||
setError(message)
|
||||
showToast(message, 'error')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
wide
|
||||
title={`Email DNS · ${domain.host}`}
|
||||
onClose={() => {
|
||||
if (!submitting) onClose()
|
||||
}}
|
||||
>
|
||||
<p className={parkedStyles.hint}>
|
||||
Paste the two DKIM lines from Stalwart (Directory → Domains → ⋮ → View DNS
|
||||
zone file). We upsert mail records on{' '}
|
||||
{dnsProvider === 'cloudflare' ? 'Cloudflare' : 'Arvan'} (DNS-only / not
|
||||
proxied): <strong>mail</strong> A → 185.214.101.41, MX → mail.meshkee.com,
|
||||
SPF, both DKIM keys, autoconfig / autodiscover. Existing DMARC is left as-is.
|
||||
</p>
|
||||
{error ? (
|
||||
<p className={tableStyles.alertError} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
{submitting ? (
|
||||
<p className={tableStyles.helperText} role="status">
|
||||
Updating {dnsProvider === 'cloudflare' ? 'Cloudflare' : 'Arvan'} mail DNS…
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className={styles.providerRow}>
|
||||
<span className={styles.providerLabel}>DNS host</span>
|
||||
<DnsProviderSwitch
|
||||
value={dnsProvider}
|
||||
onChange={setDnsProvider}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={tableStyles.field}>
|
||||
<label htmlFor="mail-dkim-ed25519">Ed25519 domain key</label>
|
||||
<textarea
|
||||
id="mail-dkim-ed25519"
|
||||
className={styles.textarea}
|
||||
value={ed25519Key}
|
||||
onChange={(e) => setEd25519Key(e.target.value)}
|
||||
placeholder="v1-ed25519-…. _domainkey IN TXT "v=DKIM1; k=ed25519; …""
|
||||
disabled={submitting}
|
||||
spellCheck={false}
|
||||
dir="ltr"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: 12 }} />
|
||||
<div className={tableStyles.field}>
|
||||
<label htmlFor="mail-dkim-rsa">RSA domain key</label>
|
||||
<textarea
|
||||
id="mail-dkim-rsa"
|
||||
className={styles.textarea}
|
||||
value={rsaKey}
|
||||
onChange={(e) => setRsaKey(e.target.value)}
|
||||
placeholder="v1-rsa-…. _domainkey IN TXT "v=DKIM1; k=rsa; …""
|
||||
disabled={submitting}
|
||||
spellCheck={false}
|
||||
dir="ltr"
|
||||
rows={7}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 12 }} />
|
||||
<div className={tableStyles.actionsRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${tableStyles.btn} ${tableStyles.btnGhost}`}
|
||||
onClick={onClose}
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${tableStyles.btn} ${tableStyles.btnPrimary}`}
|
||||
onClick={() => void submit()}
|
||||
disabled={submitting || !ed25519Key.trim() || !rsaKey.trim()}
|
||||
>
|
||||
Update DNS
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user