Initial commit: Meshkee dashboards monorepo.

Includes business, customer, and super-admin apps with shared packages and production deploy scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-07-22 13:48:53 +03:30
co-authored by Cursor
commit f566387c61
509 changed files with 62690 additions and 0 deletions
@@ -0,0 +1,977 @@
import { useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
AlertTriangle,
Globe,
Lock,
Pencil,
Plus,
RotateCcw,
Search,
Trash2,
UserCog,
Users,
} from 'lucide-react'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { ToggleSwitch } from '../components/ToggleSwitch'
import { formatCellForDisplay, toE164CellNumber } from '../lib/cellNumber'
import type { BusinessesListResponse, BusinessListItem } from '../types/business'
import type { BusinessCategory } from '../types/category'
import type { ListBusinessesParams } from '../services/businessService'
import { ApiError, isAbortError } from '../lib/api'
import {
addBusinessDomain,
createBusiness,
listBusinesses,
removeBusiness,
setBusinessActive,
updateBusiness,
updateBusinessDomain,
} from '../services/businessService'
import { listBusinessCategories } from '../services/categoryService'
import { Modal } from '../components/Modal'
import { MultiSelectDropdown } from '../components/MultiSelectDropdown'
import { PrimaryColorPicker } from '../components/PrimaryColorPicker'
import { PrimaryColorSwatchControl } from '../components/PrimaryColorSwatchControl'
import { useToast } from '../context/ToastContext'
import {
DEFAULT_BUSINESS_PRIMARY_COLOR_ID,
normalizeBusinessPrimaryColorId,
type BusinessPrimaryColorId,
} from '../utils/businessPrimaryColors'
import {
getBusinessSettings,
updateBusinessPrimaryColor,
} from '../services/businessSettingsService'
import { flattenBusinessCategories } from '../utils/categories'
import pageStyles from '../components/PageContent.module.css'
import styles from './BusinessesPage.module.css'
const DEFAULT_PAGE_SIZE = 10
function formatDate(value: string) {
const d = new Date(value)
if (Number.isNaN(d.getTime())) return value
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' })
}
function buildPageNumbers(page: number, totalPages: number) {
const start = Math.max(1, page - 2)
const end = Math.min(totalPages, page + 2)
const numbers: number[] = []
for (let i = start; i <= end; i++) numbers.push(i)
return numbers
}
export function BusinessesPage() {
const { showToast } = useToast()
const navigate = useNavigate()
const [data, setData] = useState<BusinessesListResponse | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [categories, setCategories] = useState<BusinessCategory[]>([])
const [appliedFilters, setAppliedFilters] = useState<Omit<ListBusinessesParams, 'page' | 'pageSize'>>(
{},
)
const [page, setPage] = useState(1)
const [pageSize] = useState(DEFAULT_PAGE_SIZE)
const [draftName, setDraftName] = useState('')
const [draftDomain, setDraftDomain] = useState('')
const [draftCategory, setDraftCategory] = useState('')
const [editOpen, setEditOpen] = useState(false)
const [editBusiness, setEditBusiness] = useState<BusinessListItem | null>(null)
const [editName, setEditName] = useState('')
const [editPrimaryColor, setEditPrimaryColor] = useState<BusinessPrimaryColorId>(
DEFAULT_BUSINESS_PRIMARY_COLOR_ID,
)
const [editLoadingSettings, setEditLoadingSettings] = useState(false)
const [editSubmitting, setEditSubmitting] = useState(false)
const [domainOpen, setDomainOpen] = useState(false)
const [domainBusiness, setDomainBusiness] = useState<BusinessListItem | null>(null)
const [domainId, setDomainId] = useState<number | null>(null)
const [domainHost, setDomainHost] = useState('')
const [domainSubmitting, setDomainSubmitting] = useState(false)
const [createOpen, setCreateOpen] = useState(false)
const [createName, setCreateName] = useState('')
const [createNameFa, setCreateNameFa] = useState('')
const [createAbout, setCreateAbout] = useState('')
const [createCategoryIds, setCreateCategoryIds] = useState<number[]>([])
const [createOwnerFirstName, setCreateOwnerFirstName] = useState('')
const [createOwnerLastName, setCreateOwnerLastName] = useState('')
const [createOwnerCell, setCreateOwnerCell] = useState('')
const [createOwnerPassword, setCreateOwnerPassword] = useState('')
const [createSubmitting, setCreateSubmitting] = useState(false)
const [removeTarget, setRemoveTarget] = useState<BusinessListItem | null>(null)
const [togglingId, setTogglingId] = useState<string | null>(null)
const [savingColorId, setSavingColorId] = useState<string | null>(null)
async function fetchList() {
setLoading(true)
setError('')
try {
const result = await listBusinesses({
page,
pageSize,
...appliedFilters,
})
setData(result)
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to load businesses.')
} finally {
setLoading(false)
}
}
useEffect(() => {
const controller = new AbortController()
async function load() {
setLoading(true)
setError('')
try {
const [result, categoryResult] = await Promise.all([
listBusinesses(
{
page,
pageSize,
...appliedFilters,
},
controller.signal,
),
listBusinessCategories(controller.signal),
])
if (controller.signal.aborted) return
setData(result)
setCategories(categoryResult.items)
} catch (err) {
if (isAbortError(err) || controller.signal.aborted) return
setError(err instanceof ApiError ? err.message : 'Unable to load businesses.')
} finally {
if (!controller.signal.aborted) setLoading(false)
}
}
void load()
return () => {
controller.abort()
}
}, [page, pageSize, appliedFilters.name, appliedFilters.domain, appliedFilters.category])
const totalPages = useMemo(() => {
const total = data?.total ?? 0
return Math.max(1, Math.ceil(total / pageSize))
}, [data?.total, pageSize])
const pageNumbers = useMemo(() => buildPageNumbers(page, totalPages), [page, totalPages])
const showingFrom = useMemo(() => {
if (!data) return 0
return (page - 1) * pageSize + 1
}, [data, page, pageSize])
const showingTo = useMemo(() => {
if (!data) return 0
return Math.min(data.total, page * pageSize)
}, [data, page, pageSize])
const categoryOptions = useMemo(() => flattenBusinessCategories(categories), [categories])
function applyFilters() {
setPage(1)
setAppliedFilters({
...(draftName.trim() ? { name: draftName.trim() } : {}),
...(draftDomain.trim() ? { domain: draftDomain.trim() } : {}),
...(draftCategory.trim() ? { category: draftCategory.trim() } : {}),
})
}
function clearFilters() {
setDraftName('')
setDraftDomain('')
setDraftCategory('')
setPage(1)
setAppliedFilters({})
}
function openEdit(b: BusinessListItem) {
setEditBusiness(b)
setEditName(b.name)
setEditPrimaryColor(normalizeBusinessPrimaryColorId(b.primaryColor))
setEditOpen(true)
setEditLoadingSettings(true)
const controller = new AbortController()
void getBusinessSettings(b.id, controller.signal)
.then((data) => {
setEditPrimaryColor(data.settings.branding.primaryColor)
})
.catch((err) => {
if (isAbortError(err)) return
setError(err instanceof ApiError ? err.message : 'Unable to load business theme.')
})
.finally(() => {
if (!controller.signal.aborted) setEditLoadingSettings(false)
})
}
function goToBusinessUsers(b: BusinessListItem, membership: 'staff' | 'all') {
const params = new URLSearchParams({
businessId: String(b.id),
membership,
businessName: b.name,
})
navigate(`/users?${params.toString()}`)
}
async function handlePrimaryColorChange(
b: BusinessListItem,
primaryColor: BusinessPrimaryColorId,
) {
const currentColor = normalizeBusinessPrimaryColorId(b.primaryColor)
if (currentColor === primaryColor) return
setSavingColorId(b.id)
setError('')
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === b.id ? { ...item, primaryColor } : item,
),
}
})
if (editBusiness?.id === b.id) {
setEditPrimaryColor(primaryColor)
}
try {
await updateBusinessPrimaryColor(b.id, primaryColor)
showToast(`Theme updated for "${b.name}".`, 'success')
} catch (err) {
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === b.id ? { ...item, primaryColor: currentColor } : item,
),
}
})
if (editBusiness?.id === b.id) {
setEditPrimaryColor(currentColor)
}
showToast(
err instanceof ApiError ? err.message : 'Unable to update theme.',
'error',
)
} finally {
setSavingColorId(null)
}
}
async function submitEdit() {
if (!editBusiness) return
setEditSubmitting(true)
setError('')
try {
await updateBusiness(editBusiness.id, { name: editName })
await updateBusinessPrimaryColor(editBusiness.id, editPrimaryColor)
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === editBusiness.id
? { ...item, name: editName.trim(), primaryColor: editPrimaryColor }
: item,
),
}
})
setEditOpen(false)
setEditBusiness(null)
showToast('Business updated.', 'success')
await fetchList()
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to update business.')
} finally {
setEditSubmitting(false)
}
}
function openDomainEdit(b: BusinessListItem) {
setDomainBusiness(b)
setDomainId(b.domainId)
setDomainHost(b.domain ?? '')
setDomainOpen(true)
}
async function submitDomain() {
if (!domainBusiness) return
const host = domainHost.trim()
if (!host) return
setDomainSubmitting(true)
setError('')
try {
if (domainId) {
await updateBusinessDomain(domainBusiness.id, domainId, { host })
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === domainBusiness.id ? { ...item, domain: host } : item,
),
}
})
showToast(`Domain updated to "${host}".`, 'success')
} else {
const created = (await addBusinessDomain(domainBusiness.id, {
host,
isPrimary: true,
})) as { id: number; host: string; sslEnabled: boolean }
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === domainBusiness.id
? {
...item,
domainId: created.id,
domain: created.host,
sslEnabled: created.sslEnabled ?? false,
}
: item,
),
}
})
showToast(`Domain "${host}" added.`, 'success')
}
setDomainOpen(false)
setDomainBusiness(null)
setDomainId(null)
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to save domain.')
} finally {
setDomainSubmitting(false)
}
}
function resetCreateForm() {
setCreateName('')
setCreateNameFa('')
setCreateAbout('')
setCreateCategoryIds([])
setCreateOwnerFirstName('')
setCreateOwnerLastName('')
setCreateOwnerCell('')
setCreateOwnerPassword('')
}
function openCreate() {
resetCreateForm()
setCreateOpen(true)
}
async function submitCreate() {
const ownerCellNumber = toE164CellNumber(createOwnerCell.trim())
if (!ownerCellNumber) return
setCreateSubmitting(true)
setError('')
try {
await createBusiness({
name: createName.trim(),
nameFa: createNameFa.trim(),
about: createAbout.trim() || undefined,
categoryIds: createCategoryIds,
ownerFirstName: createOwnerFirstName.trim(),
ownerLastName: createOwnerLastName.trim(),
ownerCellNumber,
ownerPassword: createOwnerPassword,
})
setCreateOpen(false)
resetCreateForm()
showToast(`"${createName.trim()}" has been created.`, 'success')
await fetchList()
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to create business.')
} finally {
setCreateSubmitting(false)
}
}
async function handleToggleActive(b: BusinessListItem, isActive: boolean) {
setTogglingId(b.id)
setError('')
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) => (item.id === b.id ? { ...item, isActive } : item)),
}
})
try {
await setBusinessActive(b.id, { isActive })
showToast(`"${b.name}" has been ${isActive ? 'enabled' : 'disabled'}.`, 'success')
} catch (err) {
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === b.id ? { ...item, isActive: !isActive } : item,
),
}
})
setError(err instanceof ApiError ? err.message : 'Unable to update business status.')
} finally {
setTogglingId(null)
}
}
async function confirmRemove() {
if (!removeTarget) return
setError('')
try {
await removeBusiness(removeTarget.id)
setRemoveTarget(null)
await fetchList()
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to remove business.')
}
}
const canSubmitCreate =
createName.trim().length >= 2 &&
createNameFa.trim().length >= 2 &&
createCategoryIds.length > 0 &&
createOwnerFirstName.trim().length >= 2 &&
createOwnerLastName.trim().length >= 2 &&
toE164CellNumber(createOwnerCell.trim()).length > 0 &&
createOwnerPassword.length >= 8
return (
<main className={pageStyles.content}>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>Businesses</h2>
<p className={pageStyles.pageSubtitle}>
Manage businesses, domains, and SSL settings.
</p>
</div>
</div>
<div className={styles.filtersPanel}>
<div className={styles.filtersTitle}>Filters</div>
<div className={styles.filtersGrid}>
<div className={styles.filtersInputs}>
<div className={`${styles.field} ${styles.fieldCol3}`}>
<label htmlFor="filter-name">Name</label>
<input
id="filter-name"
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
placeholder="Search by business name"
/>
</div>
<div className={`${styles.field} ${styles.fieldCol3}`}>
<label htmlFor="filter-domain">Domain</label>
<input
id="filter-domain"
value={draftDomain}
onChange={(e) => setDraftDomain(e.target.value)}
placeholder="shop-a.local"
/>
</div>
<div className={`${styles.field} ${styles.fieldCol3}`}>
<label htmlFor="filter-category">Category</label>
<input
id="filter-category"
value={draftCategory}
onChange={(e) => setDraftCategory(e.target.value)}
placeholder="e.g. electronics / phones"
/>
</div>
</div>
<div className={styles.filterActions}>
<button
type="button"
className={`${styles.iconActionBtn} ${styles.iconActionBtnPrimary}`}
onClick={applyFilters}
disabled={loading}
aria-label="Search"
title="Search"
>
<Search size={18} />
</button>
<button
type="button"
className={`${styles.iconActionBtn} ${styles.iconActionBtnGhost}`}
onClick={clearFilters}
disabled={loading}
aria-label="Clear filters"
title="Clear filters"
>
<RotateCcw size={18} />
</button>
</div>
</div>
</div>
<div className={styles.tablePanel}>
<div className={styles.tableWrap}>
<div className={styles.tableHeader}>
<div className={styles.tableHeaderTitle}>Business list</div>
<div className={styles.meta}>
{data ? (
<>
Showing {showingFrom} - {showingTo} of {data.total}
</>
) : (
' '
)}
</div>
</div>
{error && (
<div style={{ padding: 16 }}>
<div className={styles.meta} style={{ color: '#b91c1c' }}>
{error}
</div>
</div>
)}
<div style={{ overflowX: 'auto' }}>
<table className={styles.table}>
<thead>
<tr>
<th className={styles.th}>Name</th>
<th className={styles.th}>Date created</th>
<th className={styles.th}>Domain</th>
<th className={styles.th}>Owner</th>
<th className={`${styles.th} ${styles.thTheme}`}>Theme</th>
<th className={`${styles.th} ${styles.thActions}`}>Actions</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td className={styles.td} colSpan={6}>
Loading...
</td>
</tr>
)}
{!loading && data?.items?.length === 0 && (
<tr>
<td className={styles.td} colSpan={6}>
No results found.
</td>
</tr>
)}
{!loading &&
data?.items?.map((b) => (
<tr key={b.id}>
<td className={styles.td}>{b.name}</td>
<td className={styles.td}>{formatDate(b.createdAt)}</td>
<td className={styles.td}>
{b.domain ? (
<div className={styles.domainCell}>
<span>{b.domain}</span>
{b.sslEnabled ? (
<Lock size={16} className={styles.sslOk} aria-label="SSL enabled" />
) : (
<AlertTriangle
size={16}
className={styles.sslWarn}
aria-label="SSL not enabled"
/>
)}
</div>
) : (
<span className={styles.subText}>No domain</span>
)}
</td>
<td className={styles.td}>
<div className={styles.ownerCell}>
<div className={styles.ownerName}>{b.ownerName ?? '—'}</div>
<div className={styles.ownerPhone}>
{b.ownerCellNumber ? formatCellForDisplay(b.ownerCellNumber) : '—'}
</div>
</div>
</td>
<td className={`${styles.td} ${styles.tdTheme}`}>
<PrimaryColorSwatchControl
value={normalizeBusinessPrimaryColorId(b.primaryColor)}
disabled={savingColorId === b.id}
onChange={(primaryColor) =>
void handlePrimaryColorChange(b, primaryColor)
}
/>
</td>
<td className={`${styles.td} ${styles.tdActions}`}>
<div className={styles.rowActions}>
<span className={styles.toggleInActions}>
<ToggleSwitch
checked={b.isActive}
disabled={togglingId === b.id}
ariaLabel={`${b.isActive ? 'Disable' : 'Enable'} ${b.name}`}
onChange={(isActive) => void handleToggleActive(b, isActive)}
/>
</span>
<button
type="button"
className={styles.controlBtn}
onClick={() => goToBusinessUsers(b, 'staff')}
title="Staff"
aria-label="View staff"
>
<UserCog size={16} />
</button>
<button
type="button"
className={styles.controlBtn}
onClick={() => goToBusinessUsers(b, 'all')}
title="All users"
aria-label="View all users"
>
<Users size={16} />
</button>
<button
type="button"
className={styles.controlBtn}
onClick={() => openEdit(b)}
title="Edit"
aria-label="Edit"
>
<Pencil size={16} />
</button>
<button
type="button"
className={styles.controlBtn}
onClick={() => openDomainEdit(b)}
title="Edit domain"
aria-label="Edit domain"
>
<Globe size={16} />
</button>
<button
type="button"
className={`${styles.controlBtn} ${styles.controlBtnDanger}`}
onClick={() => setRemoveTarget(b)}
title="Remove"
aria-label="Remove"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className={styles.pagination}>
<div>
Page {page} / {totalPages}
</div>
<div className={styles.pagerBtns}>
<button
type="button"
className={styles.pageBtn}
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1 || loading}
>
Prev
</button>
{pageNumbers.map((n) => (
<button
key={n}
type="button"
className={`${styles.pageBtn} ${n === page ? styles.pageBtnActive : ''}`}
onClick={() => setPage(n)}
disabled={loading}
>
{n}
</button>
))}
<button
type="button"
className={styles.pageBtn}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages || loading}
>
Next
</button>
</div>
</div>
</div>
</div>
<button
type="button"
className={styles.fab}
onClick={openCreate}
aria-label="Add business"
title="Add business"
>
<Plus size={24} />
</button>
<Modal
open={editOpen}
title="Edit business"
onClose={() => {
setEditOpen(false)
setEditBusiness(null)
}}
>
<div className={styles.field}>
<label htmlFor="edit-name">Business name</label>
<input
id="edit-name"
value={editName}
onChange={(e) => setEditName(e.target.value)}
disabled={editSubmitting}
/>
</div>
<div className={styles.field}>
<label>Dashboard primary color</label>
{editLoadingSettings ? (
<p className={styles.helperText}>Loading theme...</p>
) : (
<PrimaryColorPicker
value={editPrimaryColor}
onChange={setEditPrimaryColor}
disabled={editSubmitting}
/>
)}
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => setEditOpen(false)}
disabled={editSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitEdit()}
disabled={editSubmitting || !editName.trim() || editLoadingSettings}
>
Save
</button>
</div>
</Modal>
<Modal
open={domainOpen}
title={domainId ? 'Edit domain' : 'Add domain'}
onClose={() => {
setDomainOpen(false)
setDomainBusiness(null)
setDomainId(null)
}}
>
<div className={styles.field}>
<label htmlFor="domain-host">Domain</label>
<input
id="domain-host"
value={domainHost}
onChange={(e) => setDomainHost(e.target.value)}
placeholder="shop-a.local"
disabled={domainSubmitting}
/>
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => setDomainOpen(false)}
disabled={domainSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitDomain()}
disabled={domainSubmitting || !domainHost.trim()}
>
Save
</button>
</div>
</Modal>
<Modal
open={createOpen}
title="Add business"
wide
onClose={() => {
setCreateOpen(false)
resetCreateForm()
}}
>
<div className={styles.formGrid}>
<div className={styles.field}>
<label htmlFor="create-name-en">Name (EN)</label>
<input
id="create-name-en"
value={createName}
onChange={(e) => setCreateName(e.target.value)}
placeholder="New Shop"
disabled={createSubmitting}
/>
</div>
<div className={styles.field}>
<label htmlFor="create-name-fa">Name (FA)</label>
<input
id="create-name-fa"
className="faText"
value={createNameFa}
onChange={(e) => setCreateNameFa(e.target.value)}
placeholder="نام فارسی"
dir="rtl"
disabled={createSubmitting}
/>
</div>
<div className={`${styles.field} ${styles.formFull}`}>
<label htmlFor="create-categories">Categories</label>
<MultiSelectDropdown
id="create-categories"
options={categoryOptions}
value={createCategoryIds}
onChange={setCreateCategoryIds}
placeholder="Select categories"
disabled={createSubmitting || categories.length === 0}
/>
</div>
<div className={`${styles.field} ${styles.formFull}`}>
<div className={styles.sectionLabel}>Owner</div>
</div>
<div className={styles.field}>
<label htmlFor="create-owner-first-name">First name</label>
<input
id="create-owner-first-name"
value={createOwnerFirstName}
onChange={(e) => setCreateOwnerFirstName(e.target.value)}
disabled={createSubmitting}
/>
</div>
<div className={styles.field}>
<label htmlFor="create-owner-last-name">Last name</label>
<input
id="create-owner-last-name"
value={createOwnerLastName}
onChange={(e) => setCreateOwnerLastName(e.target.value)}
disabled={createSubmitting}
/>
</div>
<div className={`${styles.field} ${styles.formFull}`}>
<label htmlFor="create-owner-cell">Cell number</label>
<input
id="create-owner-cell"
value={createOwnerCell}
onChange={(e) => setCreateOwnerCell(e.target.value)}
placeholder="0912..."
name="owner-cell"
autoComplete="off"
disabled={createSubmitting}
/>
</div>
<div className={`${styles.field} ${styles.formFull}`}>
<label htmlFor="create-owner-password">Password</label>
<input
id="create-owner-password"
type="password"
value={createOwnerPassword}
onChange={(e) => setCreateOwnerPassword(e.target.value)}
placeholder="Min. 8 characters"
name="owner-password"
autoComplete="new-password"
disabled={createSubmitting}
/>
</div>
<div className={`${styles.field} ${styles.formFull}`}>
<label htmlFor="create-about">About business</label>
<textarea
id="create-about"
value={createAbout}
onChange={(e) => setCreateAbout(e.target.value)}
placeholder="Short description about the business"
disabled={createSubmitting}
/>
</div>
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => setCreateOpen(false)}
disabled={createSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitCreate()}
disabled={createSubmitting || !canSubmitCreate}
>
Create
</button>
</div>
</Modal>
<ConfirmDeleteModal
open={removeTarget !== null}
title="Remove business?"
message={
removeTarget
? `Are you sure you want to remove "${removeTarget.name}"? This action cannot be undone.`
: ''
}
onCancel={() => setRemoveTarget(null)}
onConfirm={() => void confirmRemove()}
/>
</main>
)
}