Add Arvan/Cloudflare DNS choice on domain edit and restyle empty add boxes.
Domain save can upsert tenant records on either provider, overflow menus stay opaque, and empty-state add controls use a dashed outline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
0d62a4ca79
commit
c962fe524b
@@ -27,18 +27,14 @@
|
|||||||
.menu {
|
.menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1200;
|
z-index: 1200;
|
||||||
|
isolation: isolate;
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
background:
|
/* Card glass (0.62) is too thin over row icons; page orbs already use
|
||||||
linear-gradient(
|
backdrop-filter, so a second blur on this panel often does nothing. */
|
||||||
180deg,
|
background: var(--toast-bg, rgba(255, 255, 255, 0.94));
|
||||||
rgba(255, 255, 255, 0.72) 0%,
|
|
||||||
var(--glass-bg-strong) 100%
|
|
||||||
);
|
|
||||||
backdrop-filter: blur(var(--blur-glass)) saturate(1.85);
|
|
||||||
-webkit-backdrop-filter: blur(var(--blur-glass)) saturate(1.85);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 18px 48px rgba(15, 23, 42, 0.2),
|
0 18px 48px rgba(15, 23, 42, 0.2),
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.45),
|
0 0 0 1px rgba(255, 255, 255, 0.45),
|
||||||
@@ -46,6 +42,17 @@
|
|||||||
animation: menuIn 0.16s ease;
|
animation: menuIn 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
backdrop-filter: blur(var(--blur-glass)) saturate(1.85);
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur-glass)) saturate(1.85);
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ParkedDnsProvider } from '../types/domain'
|
||||||
|
import styles from './ParkedDomainsModal.module.css'
|
||||||
|
|
||||||
|
interface DnsProviderSwitchProps {
|
||||||
|
value: ParkedDnsProvider
|
||||||
|
onChange: (dns: ParkedDnsProvider) => void
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DnsProviderSwitch({ value, onChange, disabled }: DnsProviderSwitchProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.dnsSwitch} role="group" aria-label="DNS provider">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.dnsOption} ${value === 'arvan' ? styles.dnsOptionOn : ''}`}
|
||||||
|
onClick={() => onChange('arvan')}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-pressed={value === 'arvan'}
|
||||||
|
>
|
||||||
|
Arvan
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.dnsOption} ${value === 'cloudflare' ? styles.dnsOptionOn : ''}`}
|
||||||
|
onClick={() => onChange('cloudflare')}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-pressed={value === 'cloudflare'}
|
||||||
|
>
|
||||||
|
Cloudflare
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Plus, X } from 'lucide-react'
|
import { Plus, X } from 'lucide-react'
|
||||||
import { Modal } from './Modal'
|
import { Modal } from './Modal'
|
||||||
|
import { DnsProviderSwitch } from './DnsProviderSwitch'
|
||||||
import { ApiError } from '../lib/api'
|
import { ApiError } from '../lib/api'
|
||||||
import { updateParkedHosts } from '../services/domainService'
|
import { updateParkedHosts } from '../services/domainService'
|
||||||
import type { DomainListItem, ParkedAlias, ParkedDnsProvider } from '../types/domain'
|
import type { DomainListItem, ParkedAlias, ParkedDnsProvider } from '../types/domain'
|
||||||
@@ -135,26 +136,11 @@ export function ParkedDomainsModal({ domain, onClose, onSaved }: ParkedDomainsMo
|
|||||||
dir="ltr"
|
dir="ltr"
|
||||||
aria-label={`Parked domain ${index + 1}`}
|
aria-label={`Parked domain ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
<div className={styles.dnsSwitch} role="group" aria-label="DNS provider">
|
<DnsProviderSwitch
|
||||||
<button
|
value={row.dns}
|
||||||
type="button"
|
onChange={(dns) => updateDns(index, dns)}
|
||||||
className={`${styles.dnsOption} ${row.dns === 'arvan' ? styles.dnsOptionOn : ''}`}
|
disabled={submitting}
|
||||||
onClick={() => updateDns(index, 'arvan')}
|
/>
|
||||||
disabled={submitting}
|
|
||||||
aria-pressed={row.dns === 'arvan'}
|
|
||||||
>
|
|
||||||
Arvan
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${styles.dnsOption} ${row.dns === 'cloudflare' ? styles.dnsOptionOn : ''}`}
|
|
||||||
onClick={() => updateDns(index, 'cloudflare')}
|
|
||||||
disabled={submitting}
|
|
||||||
aria-pressed={row.dns === 'cloudflare'}
|
|
||||||
>
|
|
||||||
Cloudflare
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.removeRowBtn}
|
className={styles.removeRowBtn}
|
||||||
|
|||||||
@@ -144,6 +144,14 @@
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dnsUpdateRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.alertError {
|
.alertError {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
PurgeBusinessDataResponse,
|
PurgeBusinessDataResponse,
|
||||||
PurgeEntityResult,
|
PurgeEntityResult,
|
||||||
} from '../types/business'
|
} from '../types/business'
|
||||||
|
import type { ParkedDnsProvider } from '../types/domain'
|
||||||
import type { BusinessCategory } from '../types/category'
|
import type { BusinessCategory } from '../types/category'
|
||||||
import type { ListBusinessesParams } from '../services/businessService'
|
import type { ListBusinessesParams } from '../services/businessService'
|
||||||
import { ApiError, isAbortError } from '../lib/api'
|
import { ApiError, isAbortError } from '../lib/api'
|
||||||
@@ -58,6 +59,7 @@ import {
|
|||||||
} from '../services/aiPromptService'
|
} from '../services/aiPromptService'
|
||||||
import type { AiPromptTemplate, DomainAiPrompt } from '../types/aiPrompt'
|
import type { AiPromptTemplate, DomainAiPrompt } from '../types/aiPrompt'
|
||||||
import { Modal } from '../components/Modal'
|
import { Modal } from '../components/Modal'
|
||||||
|
import { DnsProviderSwitch } from '../components/DnsProviderSwitch'
|
||||||
import { PrimaryColorSwatchControl } from '../components/PrimaryColorSwatchControl'
|
import { PrimaryColorSwatchControl } from '../components/PrimaryColorSwatchControl'
|
||||||
import { useToast } from '../context/ToastContext'
|
import { useToast } from '../context/ToastContext'
|
||||||
import {
|
import {
|
||||||
@@ -346,6 +348,7 @@ export function BusinessesPage() {
|
|||||||
const [domainHost, setDomainHost] = useState('')
|
const [domainHost, setDomainHost] = useState('')
|
||||||
const [domainGitRepoUrl, setDomainGitRepoUrl] = useState('')
|
const [domainGitRepoUrl, setDomainGitRepoUrl] = useState('')
|
||||||
const [domainUpdateDns, setDomainUpdateDns] = useState(false)
|
const [domainUpdateDns, setDomainUpdateDns] = useState(false)
|
||||||
|
const [domainDnsProvider, setDomainDnsProvider] = useState<ParkedDnsProvider>('arvan')
|
||||||
const [domainSubmitting, setDomainSubmitting] = useState(false)
|
const [domainSubmitting, setDomainSubmitting] = useState(false)
|
||||||
const [domainError, setDomainError] = useState('')
|
const [domainError, setDomainError] = useState('')
|
||||||
|
|
||||||
@@ -933,6 +936,7 @@ export function BusinessesPage() {
|
|||||||
setDomainHost(b.domain ?? '')
|
setDomainHost(b.domain ?? '')
|
||||||
setDomainGitRepoUrl(b.gitRepoUrl ?? '')
|
setDomainGitRepoUrl(b.gitRepoUrl ?? '')
|
||||||
setDomainUpdateDns(!b.domainId)
|
setDomainUpdateDns(!b.domainId)
|
||||||
|
setDomainDnsProvider('arvan')
|
||||||
setDomainError('')
|
setDomainError('')
|
||||||
setDomainOpen(true)
|
setDomainOpen(true)
|
||||||
}
|
}
|
||||||
@@ -1126,6 +1130,7 @@ export function BusinessesPage() {
|
|||||||
setDomainHost('')
|
setDomainHost('')
|
||||||
setDomainGitRepoUrl('')
|
setDomainGitRepoUrl('')
|
||||||
setDomainUpdateDns(false)
|
setDomainUpdateDns(false)
|
||||||
|
setDomainDnsProvider('arvan')
|
||||||
setDomainError('')
|
setDomainError('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,6 +1149,7 @@ export function BusinessesPage() {
|
|||||||
const updated = (await updateBusinessDomain(businessId, domainId, {
|
const updated = (await updateBusinessDomain(businessId, domainId, {
|
||||||
host,
|
host,
|
||||||
updateDns: domainUpdateDns,
|
updateDns: domainUpdateDns,
|
||||||
|
...(domainUpdateDns ? { dnsProvider: domainDnsProvider } : {}),
|
||||||
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
||||||
})) as {
|
})) as {
|
||||||
id: number
|
id: number
|
||||||
@@ -1193,6 +1199,7 @@ export function BusinessesPage() {
|
|||||||
host,
|
host,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
updateDns: domainUpdateDns,
|
updateDns: domainUpdateDns,
|
||||||
|
...(domainUpdateDns ? { dnsProvider: domainDnsProvider } : {}),
|
||||||
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
||||||
})) as {
|
})) as {
|
||||||
id: number
|
id: number
|
||||||
@@ -1248,6 +1255,7 @@ export function BusinessesPage() {
|
|||||||
setDomainHost('')
|
setDomainHost('')
|
||||||
setDomainGitRepoUrl('')
|
setDomainGitRepoUrl('')
|
||||||
setDomainUpdateDns(false)
|
setDomainUpdateDns(false)
|
||||||
|
setDomainDnsProvider('arvan')
|
||||||
setDomainError('')
|
setDomainError('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (submitId !== domainSubmitGenRef.current) return
|
if (submitId !== domainSubmitGenRef.current) return
|
||||||
@@ -1998,7 +2006,7 @@ export function BusinessesPage() {
|
|||||||
<p className={styles.helperText} role="status">
|
<p className={styles.helperText} role="status">
|
||||||
{domainGitRepoUrl.trim()
|
{domainGitRepoUrl.trim()
|
||||||
? 'Saving domain and setting up storefront on the websites server…'
|
? 'Saving domain and setting up storefront on the websites server…'
|
||||||
: 'Saving domain and updating Arvan DNS…'}
|
: `Saving domain and updating ${domainDnsProvider === 'cloudflare' ? 'Cloudflare' : 'Arvan'} DNS…`}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
@@ -2029,19 +2037,27 @@ export function BusinessesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 12 }} />
|
<div style={{ height: 12 }} />
|
||||||
<label className={styles.entityCheck} htmlFor="domain-update-dns">
|
<div className={styles.dnsUpdateRow}>
|
||||||
<input
|
<label className={styles.entityCheck} htmlFor="domain-update-dns">
|
||||||
id="domain-update-dns"
|
<input
|
||||||
type="checkbox"
|
id="domain-update-dns"
|
||||||
checked={domainUpdateDns}
|
type="checkbox"
|
||||||
onChange={(e) => setDomainUpdateDns(e.target.checked)}
|
checked={domainUpdateDns}
|
||||||
disabled={domainSubmitting}
|
onChange={(e) => setDomainUpdateDns(e.target.checked)}
|
||||||
|
disabled={domainSubmitting}
|
||||||
|
/>
|
||||||
|
<span>Update DNS records</span>
|
||||||
|
</label>
|
||||||
|
<DnsProviderSwitch
|
||||||
|
value={domainDnsProvider}
|
||||||
|
onChange={setDomainDnsProvider}
|
||||||
|
disabled={domainSubmitting || !domainUpdateDns}
|
||||||
/>
|
/>
|
||||||
<span>Update DNS records</span>
|
</div>
|
||||||
</label>
|
|
||||||
<p className={styles.subText} style={{ marginTop: 6 }}>
|
<p className={styles.subText} style={{ marginTop: 6 }}>
|
||||||
When checked, upserts Arvan records: @ ANAME → ns.meshkee.com, plus www,
|
{domainDnsProvider === 'cloudflare'
|
||||||
business, customer, and api CNAMEs.
|
? 'When checked, upserts Cloudflare records (DNS-only): @ CNAME → ns.meshkee.com, plus www, business, customer, and api CNAMEs.'
|
||||||
|
: 'When checked, upserts Arvan records: @ ANAME → ns.meshkee.com, plus www, business, customer, and api CNAMEs.'}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ height: 12 }} />
|
<div style={{ height: 12 }} />
|
||||||
<div className={styles.actionsRow}>
|
<div className={styles.actionsRow}>
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ import {
|
|||||||
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
||||||
import { DomainAiPromptsModal } from '../components/DomainAiPromptsModal'
|
import { DomainAiPromptsModal } from '../components/DomainAiPromptsModal'
|
||||||
import { DomainLinksOverflowMenu } from '../components/DomainLinksOverflowMenu'
|
import { DomainLinksOverflowMenu } from '../components/DomainLinksOverflowMenu'
|
||||||
|
import { DnsProviderSwitch } from '../components/DnsProviderSwitch'
|
||||||
import { ParkedDomainsModal } from '../components/ParkedDomainsModal'
|
import { ParkedDomainsModal } from '../components/ParkedDomainsModal'
|
||||||
import { Modal } from '../components/Modal'
|
import { Modal } from '../components/Modal'
|
||||||
import { ToggleSwitch } from '../components/ToggleSwitch'
|
import { ToggleSwitch } from '../components/ToggleSwitch'
|
||||||
import type { DomainListItem, DomainsListResponse } from '../types/domain'
|
import type { DomainListItem, DomainsListResponse, ParkedDnsProvider } from '../types/domain'
|
||||||
import type { ListDomainsParams } from '../services/domainService'
|
import type { ListDomainsParams } from '../services/domainService'
|
||||||
import { ApiError, isAbortError } from '../lib/api'
|
import { ApiError, isAbortError } from '../lib/api'
|
||||||
import {
|
import {
|
||||||
@@ -106,6 +107,7 @@ export function WebsitesPage() {
|
|||||||
const [editExpiresAt, setEditExpiresAt] = useState('')
|
const [editExpiresAt, setEditExpiresAt] = useState('')
|
||||||
const [editGitRepoUrl, setEditGitRepoUrl] = useState('')
|
const [editGitRepoUrl, setEditGitRepoUrl] = useState('')
|
||||||
const [editUpdateDns, setEditUpdateDns] = useState(false)
|
const [editUpdateDns, setEditUpdateDns] = useState(false)
|
||||||
|
const [editDnsProvider, setEditDnsProvider] = useState<ParkedDnsProvider>('arvan')
|
||||||
const [editSubmitting, setEditSubmitting] = useState(false)
|
const [editSubmitting, setEditSubmitting] = useState(false)
|
||||||
const [editError, setEditError] = useState('')
|
const [editError, setEditError] = useState('')
|
||||||
|
|
||||||
@@ -179,6 +181,7 @@ export function WebsitesPage() {
|
|||||||
setEditExpiresAt(domain.expiresAt ? domain.expiresAt.slice(0, 10) : '')
|
setEditExpiresAt(domain.expiresAt ? domain.expiresAt.slice(0, 10) : '')
|
||||||
setEditGitRepoUrl(domain.gitRepoUrl ?? '')
|
setEditGitRepoUrl(domain.gitRepoUrl ?? '')
|
||||||
setEditUpdateDns(false)
|
setEditUpdateDns(false)
|
||||||
|
setEditDnsProvider('arvan')
|
||||||
setEditError('')
|
setEditError('')
|
||||||
setEditOpen(true)
|
setEditOpen(true)
|
||||||
}
|
}
|
||||||
@@ -192,6 +195,7 @@ export function WebsitesPage() {
|
|||||||
const updated = (await updateDomain(editDomain.id, {
|
const updated = (await updateDomain(editDomain.id, {
|
||||||
host: editHost.trim(),
|
host: editHost.trim(),
|
||||||
updateDns: editUpdateDns,
|
updateDns: editUpdateDns,
|
||||||
|
...(editUpdateDns ? { dnsProvider: editDnsProvider } : {}),
|
||||||
...(editExpiresAt ? { expiresAt: new Date(editExpiresAt).toISOString() } : {}),
|
...(editExpiresAt ? { expiresAt: new Date(editExpiresAt).toISOString() } : {}),
|
||||||
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
||||||
})) as {
|
})) as {
|
||||||
@@ -204,6 +208,7 @@ export function WebsitesPage() {
|
|||||||
setEditDomain(null)
|
setEditDomain(null)
|
||||||
setEditGitRepoUrl('')
|
setEditGitRepoUrl('')
|
||||||
setEditUpdateDns(false)
|
setEditUpdateDns(false)
|
||||||
|
setEditDnsProvider('arvan')
|
||||||
if (updated.provisionError) {
|
if (updated.provisionError) {
|
||||||
showToast(`"${editHost.trim()}" was updated, but storefront setup failed.`, 'error')
|
showToast(`"${editHost.trim()}" was updated, but storefront setup failed.`, 'error')
|
||||||
showToast(updated.provisionError, 'error')
|
showToast(updated.provisionError, 'error')
|
||||||
@@ -806,6 +811,7 @@ export function WebsitesPage() {
|
|||||||
setEditDomain(null)
|
setEditDomain(null)
|
||||||
setEditGitRepoUrl('')
|
setEditGitRepoUrl('')
|
||||||
setEditUpdateDns(false)
|
setEditUpdateDns(false)
|
||||||
|
setEditDnsProvider('arvan')
|
||||||
setEditError('')
|
setEditError('')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -818,7 +824,7 @@ export function WebsitesPage() {
|
|||||||
<p className={tableStyles.helperText} role="status">
|
<p className={tableStyles.helperText} role="status">
|
||||||
{editGitRepoUrl.trim()
|
{editGitRepoUrl.trim()
|
||||||
? 'Saving domain and setting up storefront on the websites server…'
|
? 'Saving domain and setting up storefront on the websites server…'
|
||||||
: 'Saving domain and updating Arvan DNS…'}
|
: `Saving domain and updating ${editDnsProvider === 'cloudflare' ? 'Cloudflare' : 'Arvan'} DNS…`}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={tableStyles.field}>
|
<div className={tableStyles.field}>
|
||||||
@@ -859,19 +865,27 @@ export function WebsitesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 12 }} />
|
<div style={{ height: 12 }} />
|
||||||
<label className={tableStyles.entityCheck} htmlFor="edit-domain-update-dns">
|
<div className={tableStyles.dnsUpdateRow}>
|
||||||
<input
|
<label className={tableStyles.entityCheck} htmlFor="edit-domain-update-dns">
|
||||||
id="edit-domain-update-dns"
|
<input
|
||||||
type="checkbox"
|
id="edit-domain-update-dns"
|
||||||
checked={editUpdateDns}
|
type="checkbox"
|
||||||
onChange={(e) => setEditUpdateDns(e.target.checked)}
|
checked={editUpdateDns}
|
||||||
disabled={editSubmitting}
|
onChange={(e) => setEditUpdateDns(e.target.checked)}
|
||||||
|
disabled={editSubmitting}
|
||||||
|
/>
|
||||||
|
<span>Update DNS records</span>
|
||||||
|
</label>
|
||||||
|
<DnsProviderSwitch
|
||||||
|
value={editDnsProvider}
|
||||||
|
onChange={setEditDnsProvider}
|
||||||
|
disabled={editSubmitting || !editUpdateDns}
|
||||||
/>
|
/>
|
||||||
<span>Update DNS records</span>
|
</div>
|
||||||
</label>
|
|
||||||
<p className={tableStyles.subText} style={{ marginTop: 6 }}>
|
<p className={tableStyles.subText} style={{ marginTop: 6 }}>
|
||||||
When checked, upserts Arvan records: @ ANAME → ns.meshkee.com, plus www,
|
{editDnsProvider === 'cloudflare'
|
||||||
business, customer, and api CNAMEs.
|
? 'When checked, upserts Cloudflare records (DNS-only): @ CNAME → ns.meshkee.com, plus www, business, customer, and api CNAMEs.'
|
||||||
|
: 'When checked, upserts Arvan records: @ ANAME → ns.meshkee.com, plus www, business, customer, and api CNAMEs.'}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ height: 12 }} />
|
<div style={{ height: 12 }} />
|
||||||
<div className={tableStyles.actionsRow}>
|
<div className={tableStyles.actionsRow}>
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ export interface UpdateBusinessPayload {
|
|||||||
export interface AddDomainPayload {
|
export interface AddDomainPayload {
|
||||||
host: string
|
host: string
|
||||||
isPrimary?: boolean
|
isPrimary?: boolean
|
||||||
/** When true, upsert Meshkee tenant records in Arvan DNS. */
|
/** When true, upsert Meshkee tenant records (@, www, business, customer, api). */
|
||||||
updateDns?: boolean
|
updateDns?: boolean
|
||||||
|
/** DNS host when updateDns is true. Default arvan. */
|
||||||
|
dnsProvider?: 'arvan' | 'cloudflare'
|
||||||
/** When set, provisions storefront deploy on the websites VM. */
|
/** When set, provisions storefront deploy on the websites VM. */
|
||||||
gitRepoUrl?: string
|
gitRepoUrl?: string
|
||||||
}
|
}
|
||||||
@@ -41,8 +43,10 @@ export interface DisableBusinessPayload {
|
|||||||
|
|
||||||
export interface UpdateDomainPayload {
|
export interface UpdateDomainPayload {
|
||||||
host: string
|
host: string
|
||||||
/** When true, upsert Meshkee tenant records in Arvan DNS. */
|
/** When true, upsert Meshkee tenant records (@, www, business, customer, api). */
|
||||||
updateDns?: boolean
|
updateDns?: boolean
|
||||||
|
/** DNS host when updateDns is true. Default arvan. */
|
||||||
|
dnsProvider?: 'arvan' | 'cloudflare'
|
||||||
/** When set, provisions storefront deploy on the websites VM. */
|
/** When set, provisions storefront deploy on the websites VM. */
|
||||||
gitRepoUrl?: string
|
gitRepoUrl?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ export interface ListDomainsParams {
|
|||||||
export interface UpdateDomainPayload {
|
export interface UpdateDomainPayload {
|
||||||
host?: string
|
host?: string
|
||||||
expiresAt?: string
|
expiresAt?: string
|
||||||
/** When true, upsert Meshkee tenant records in Arvan DNS. */
|
/** When true, upsert Meshkee tenant records (@, www, business, customer, api). */
|
||||||
updateDns?: boolean
|
updateDns?: boolean
|
||||||
|
/** DNS host when updateDns is true. Default arvan. */
|
||||||
|
dnsProvider?: 'arvan' | 'cloudflare'
|
||||||
/** When set, provisions storefront deploy on the websites VM. */
|
/** When set, provisions storefront deploy on the websites VM. */
|
||||||
gitRepoUrl?: string
|
gitRepoUrl?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,11 +138,11 @@ Add to `/etc/hosts` (one line per tenant):
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `/login` | Login |
|
| `/login` | Login |
|
||||||
| `/` | Home |
|
| `/` | Home |
|
||||||
| `/businesses` | Businesses list (migrate-from-old + delete-data for portfolios/blogs(+news)/customers + categories; single-select). **Add/Edit domain** accepts optional Git repo URL → provisions storefront on websites VM and sets `deploy_slug`. Optional **Update DNS records** checkbox upserts Arvan DNS (`@` ANAME → `ns.meshkee.com`, `www` CNAME → apex, `business`/`customer`/`api` CNAMEs). |
|
| `/businesses` | Businesses list (migrate-from-old + delete-data for portfolios/blogs(+news)/customers + categories; single-select). **Add/Edit domain** accepts optional Git repo URL → provisions storefront on websites VM and sets `deploy_slug`. Optional **Update DNS records** checkbox upserts tenant DNS (`@` → `ns.meshkee.com`, `www` CNAME → apex, `business`/`customer`/`api` CNAMEs) via **Arvan** or **Cloudflare** (switch; Cloudflare is DNS-only / not proxied). |
|
||||||
| `/businesses/:businessId/invoices` | Business invoices list |
|
| `/businesses/:businessId/invoices` | Business invoices list |
|
||||||
| `/businesses/:businessId/invoices/new` | Issue invoice (full page) |
|
| `/businesses/:businessId/invoices/new` | Issue invoice (full page) |
|
||||||
| `/users` | Users (business filter: Customer vs Manager → Admin/Editor/Viewer; Admin assignable by super-admin only; owners locked) |
|
| `/users` | Users (business filter: Customer vs Manager → Admin/Editor/Viewer; Admin assignable by super-admin only; owners locked) |
|
||||||
| `/websites` | Websites / domains (Deploy when `deploy_slug` set). Edit domain can optionally update Arvan DNS. Overflow ⋯ includes website/dashboard links, **AI prompts**, and remove. **Parked domains** button: alias hosts 301 to the main domain. Per alias, choose **Arvan** or **Cloudflare** for DNS (`@` + `www` only, not proxied); SSL is always Let's Encrypt on the websites VM. |
|
| `/websites` | Websites / domains (Deploy when `deploy_slug` set). Edit domain can optionally update DNS (Arvan or Cloudflare, same tenant records as Businesses). Overflow ⋯ includes website/dashboard links, **AI prompts**, and remove. **Parked domains** button: alias hosts 301 to the main domain. Per alias, choose **Arvan** or **Cloudflare** for DNS (`@` + `www` only, not proxied); SSL is always Let's Encrypt on the websites VM. |
|
||||||
| `/templates` | Global AI prompt templates. Assign from Businesses or Websites ⋯ → **AI prompts** (add from library; public pack link). |
|
| `/templates` | Global AI prompt templates. Assign from Businesses or Websites ⋯ → **AI prompts** (add from library; public pack link). |
|
||||||
| `/settings` | Platform settings (invoice templates + item templates) |
|
| `/settings` | Platform settings (invoice templates + item templates) |
|
||||||
| `/settings/invoice-templates/new` | Create invoice template |
|
| `/settings/invoice-templates/new` | Create invoice template |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 32px 0 48px;
|
padding: 20px 0 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rootDashed {
|
.rootDashed {
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 24px;
|
gap: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 880px;
|
max-width: 880px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
.rootDashedInline {
|
.rootDashedInline {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 24px 12px;
|
padding: 12px 8px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messageDashed {
|
.messageDashed {
|
||||||
@@ -55,36 +55,52 @@
|
|||||||
|
|
||||||
.addBox {
|
.addBox {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 6px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 880px;
|
max-width: 480px;
|
||||||
min-height: 180px;
|
min-height: 76px;
|
||||||
padding: 32px 28px;
|
padding: 12px 14px;
|
||||||
background: var(--glass-bg);
|
background: none;
|
||||||
backdrop-filter: blur(20px);
|
background-color: transparent;
|
||||||
-webkit-backdrop-filter: blur(20px);
|
border-width: 2px;
|
||||||
border: 2px dashed rgba(var(--primary-rgb) / 0.38);
|
border-style: dashed;
|
||||||
border-radius: var(--radius);
|
border-color: #64748b;
|
||||||
color: var(--primary);
|
border-radius: 12px;
|
||||||
|
color: var(--primary, #0a1628);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background 0.2s ease,
|
background-color 0.15s ease,
|
||||||
border-color 0.2s ease,
|
border-color 0.15s ease,
|
||||||
transform 0.2s ease,
|
color 0.15s ease,
|
||||||
box-shadow 0.2s ease;
|
box-shadow 0.15s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addBox:hover {
|
a.addBox,
|
||||||
background: rgba(var(--primary-rgb) / 0.08);
|
button.addBox {
|
||||||
border-color: rgba(var(--primary-rgb) / 0.58);
|
background: none;
|
||||||
transform: translateY(-3px);
|
background-color: transparent;
|
||||||
box-shadow: var(--card-hover-shadow);
|
border-width: 2px;
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addBox:hover,
|
||||||
|
a.addBox:hover,
|
||||||
|
button.addBox:hover {
|
||||||
|
background-color: rgba(10, 22, 40, 0.07);
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: #0a1628;
|
||||||
|
color: #0a1628;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.addBox:focus-visible {
|
.addBox:focus-visible {
|
||||||
@@ -96,16 +112,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 52px;
|
width: 28px;
|
||||||
height: 52px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(var(--primary-rgb) / 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.addLabel {
|
.addLabel {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
@@ -127,4 +141,8 @@
|
|||||||
.addBox {
|
.addBox {
|
||||||
max-width: min(100%, 640px);
|
max-width: min(100%, 640px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addBox {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function EmptyStateContent({
|
|||||||
const addContent = (
|
const addContent = (
|
||||||
<>
|
<>
|
||||||
<span className={styles.addIcon} aria-hidden="true">
|
<span className={styles.addIcon} aria-hidden="true">
|
||||||
<Plus size={28} strokeWidth={2} />
|
<Plus size={20} strokeWidth={2} />
|
||||||
</span>
|
</span>
|
||||||
{actionLabel ? <span className={styles.addLabel}>{actionLabel}</span> : null}
|
{actionLabel ? <span className={styles.addLabel}>{actionLabel}</span> : null}
|
||||||
</>
|
</>
|
||||||
@@ -92,11 +92,11 @@ function EmptyStateContent({
|
|||||||
>
|
>
|
||||||
<p className={styles.messageDashed}>{messageContent}</p>
|
<p className={styles.messageDashed}>{messageContent}</p>
|
||||||
{actionTo ? (
|
{actionTo ? (
|
||||||
<Link to={actionTo} className={styles.addBox} onClick={onAction}>
|
<Link to={actionTo} className={styles.addBox} data-empty-add="" onClick={onAction}>
|
||||||
{addContent}
|
{addContent}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" className={styles.addBox} onClick={onAction}>
|
<button type="button" className={styles.addBox} data-empty-add="" onClick={onAction}>
|
||||||
{addContent}
|
{addContent}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user