Add workshop generate-link type, Email DNS for domains, and editor polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-01 22:00:04 +03:30
co-authored by Cursor
parent 6be867ea7a
commit 3cbadb4001
25 changed files with 467 additions and 70 deletions
@@ -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 &quot;v=DKIM1; k=ed25519; …&quot;"
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 &quot;v=DKIM1; k=rsa; …&quot;"
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>
)
}