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
+527
View File
@@ -0,0 +1,527 @@
import { useEffect, useMemo, useState } from 'react'
import { AlertTriangle, Lock, Pencil, RotateCcw, Search, Trash2, Unlock } from 'lucide-react'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { Modal } from '../components/Modal'
import { ToggleSwitch } from '../components/ToggleSwitch'
import type { DomainListItem, DomainsListResponse } from '../types/domain'
import type { ListDomainsParams } from '../services/domainService'
import { ApiError, isAbortError } from '../lib/api'
import {
listDomains,
removeDomain,
setDomainActive,
setDomainSsl,
updateDomain,
} from '../services/domainService'
import { useToast } from '../context/ToastContext'
import pageStyles from '../components/PageContent.module.css'
import tableStyles from './BusinessesPage.module.css'
import styles from './WebsitesPage.module.css'
const PAGE_SIZE = 10
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
}
function daysUntilExpiry(expiresAt: string | null) {
if (!expiresAt) return null
const diff = new Date(expiresAt).getTime() - Date.now()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
function formatDaysLeft(expiresAt: string | null) {
const days = daysUntilExpiry(expiresAt)
if (days === null) return '—'
if (days < 0) return 'Expired'
if (days === 0) return 'Today'
if (days === 1) return '1 day'
return `${days} days`
}
function daysLeftClass(expiresAt: string | null) {
const days = daysUntilExpiry(expiresAt)
if (days === null) return styles.daysOk
if (days < 0) return styles.daysExpired
if (days <= 30) return styles.daysWarn
return styles.daysOk
}
export function WebsitesPage() {
const { showToast } = useToast()
const [data, setData] = useState<DomainsListResponse | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [appliedFilters, setAppliedFilters] = useState<Omit<ListDomainsParams, 'page' | 'pageSize'>>({})
const [page, setPage] = useState(1)
const [draftName, setDraftName] = useState('')
const [editOpen, setEditOpen] = useState(false)
const [editDomain, setEditDomain] = useState<DomainListItem | null>(null)
const [editHost, setEditHost] = useState('')
const [editExpiresAt, setEditExpiresAt] = useState('')
const [editSubmitting, setEditSubmitting] = useState(false)
const [removeTarget, setRemoveTarget] = useState<DomainListItem | null>(null)
const [togglingActiveId, setTogglingActiveId] = useState<number | null>(null)
const [togglingSslId, setTogglingSslId] = useState<number | null>(null)
useEffect(() => {
const controller = new AbortController()
async function load() {
setLoading(true)
setError('')
try {
const result = await listDomains(
{ page, pageSize: PAGE_SIZE, ...appliedFilters },
controller.signal,
)
if (controller.signal.aborted) return
setData(result)
} catch (err) {
if (isAbortError(err) || controller.signal.aborted) return
setError(err instanceof ApiError ? err.message : 'Unable to load domains.')
} finally {
if (!controller.signal.aborted) setLoading(false)
}
}
void load()
return () => {
controller.abort()
}
}, [page, appliedFilters.name])
const totalPages = useMemo(() => {
const total = data?.total ?? 0
return Math.max(1, Math.ceil(total / PAGE_SIZE))
}, [data?.total])
const pageNumbers = useMemo(() => buildPageNumbers(page, totalPages), [page, totalPages])
const showingFrom = useMemo(() => {
if (!data) return 0
return (page - 1) * PAGE_SIZE + 1
}, [data, page])
const showingTo = useMemo(() => {
if (!data) return 0
return Math.min(data.total, page * PAGE_SIZE)
}, [data, page])
function applyFilters() {
setPage(1)
setAppliedFilters(draftName.trim() ? { name: draftName.trim() } : {})
}
function clearFilters() {
setDraftName('')
setPage(1)
setAppliedFilters({})
}
function openEdit(domain: DomainListItem) {
setEditDomain(domain)
setEditHost(domain.host)
setEditExpiresAt(domain.expiresAt ? domain.expiresAt.slice(0, 10) : '')
setEditOpen(true)
}
async function submitEdit() {
if (!editDomain) return
setEditSubmitting(true)
setError('')
try {
await updateDomain(editDomain.id, {
host: editHost.trim(),
...(editExpiresAt ? { expiresAt: new Date(editExpiresAt).toISOString() } : {}),
})
setEditOpen(false)
setEditDomain(null)
showToast(`"${editHost.trim()}" has been updated.`, 'success')
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === editDomain.id
? {
...item,
host: editHost.trim(),
expiresAt: editExpiresAt ? new Date(editExpiresAt).toISOString() : item.expiresAt,
}
: item,
),
}
})
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to update domain.')
} finally {
setEditSubmitting(false)
}
}
async function handleToggleActive(domain: DomainListItem, isActive: boolean) {
setTogglingActiveId(domain.id)
setError('')
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) => (item.id === domain.id ? { ...item, isActive } : item)),
}
})
try {
await setDomainActive(domain.id, isActive)
showToast(`"${domain.host}" has been ${isActive ? 'enabled' : 'disabled'}.`, 'success')
} catch (err) {
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === domain.id ? { ...item, isActive: !isActive } : item,
),
}
})
setError(err instanceof ApiError ? err.message : 'Unable to update domain status.')
} finally {
setTogglingActiveId(null)
}
}
async function handleToggleSsl(domain: DomainListItem, sslEnabled: boolean) {
setTogglingSslId(domain.id)
setError('')
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) => (item.id === domain.id ? { ...item, sslEnabled } : item)),
}
})
try {
await setDomainSsl(domain.id, sslEnabled)
showToast(
`SSL ${sslEnabled ? 'enabled' : 'disabled'} for "${domain.host}".`,
'success',
)
} catch (err) {
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === domain.id ? { ...item, sslEnabled: !sslEnabled } : item,
),
}
})
setError(err instanceof ApiError ? err.message : 'Unable to update SSL status.')
} finally {
setTogglingSslId(null)
}
}
async function confirmRemove() {
if (!removeTarget) return
setError('')
try {
await removeDomain(removeTarget.id)
setRemoveTarget(null)
showToast(`"${removeTarget.host}" has been removed.`, 'success')
setData((prev) => {
if (!prev) return prev
return {
...prev,
total: Math.max(0, prev.total - 1),
items: prev.items.filter((item) => item.id !== removeTarget.id),
}
})
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Unable to remove domain.')
}
}
return (
<main className={pageStyles.content}>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>Websites</h2>
<p className={pageStyles.pageSubtitle}>
Monitor and manage all domains across the platform.
</p>
</div>
</div>
<div className={tableStyles.filtersPanel}>
<div className={tableStyles.filtersTitle}>Filters</div>
<div className={tableStyles.filtersGrid}>
<div className={tableStyles.filtersInputs}>
<div className={`${tableStyles.field} ${tableStyles.fieldCol3}`}>
<label htmlFor="filter-domain-name">Name</label>
<input
id="filter-domain-name"
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
placeholder="shop-a.local"
/>
</div>
</div>
<div className={tableStyles.filterActions}>
<button
type="button"
className={`${tableStyles.iconActionBtn} ${tableStyles.iconActionBtnPrimary}`}
onClick={applyFilters}
disabled={loading}
aria-label="Search"
title="Search"
>
<Search size={18} />
</button>
<button
type="button"
className={`${tableStyles.iconActionBtn} ${tableStyles.iconActionBtnGhost}`}
onClick={clearFilters}
disabled={loading}
aria-label="Clear filters"
title="Clear filters"
>
<RotateCcw size={18} />
</button>
</div>
</div>
</div>
<div className={tableStyles.tablePanel}>
<div className={tableStyles.tableWrap}>
<div className={tableStyles.tableHeader}>
<div className={tableStyles.tableHeaderTitle}>Domain list</div>
<div className={tableStyles.meta}>
{data ? (
<>
Showing {showingFrom} - {showingTo} of {data.total}
</>
) : (
' '
)}
</div>
</div>
{error && (
<div style={{ padding: 16 }}>
<div className={tableStyles.meta} style={{ color: '#b91c1c' }}>
{error}
</div>
</div>
)}
<div style={{ overflowX: 'auto' }}>
<table className={tableStyles.table}>
<thead>
<tr>
<th className={tableStyles.th}>Name</th>
<th className={tableStyles.th}>Owner business</th>
<th className={tableStyles.th}>Days to expire</th>
<th className={tableStyles.th}>SSL</th>
<th className={`${tableStyles.th} ${tableStyles.thActions}`}>Actions</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td className={tableStyles.td} colSpan={5}>
Loading...
</td>
</tr>
)}
{!loading && data?.items?.length === 0 && (
<tr>
<td className={tableStyles.td} colSpan={5}>
No results found.
</td>
</tr>
)}
{!loading &&
data?.items?.map((domain) => (
<tr
key={domain.id}
className={!domain.isActive ? styles.inactiveRow : undefined}
>
<td className={tableStyles.td}>{domain.host}</td>
<td className={tableStyles.td}>{domain.businessName}</td>
<td className={tableStyles.td}>
<span className={daysLeftClass(domain.expiresAt)}>
{formatDaysLeft(domain.expiresAt)}
</span>
</td>
<td className={tableStyles.td}>
<div className={tableStyles.domainCell}>
{domain.sslEnabled ? (
<Lock size={16} className={tableStyles.sslOk} aria-label="SSL enabled" />
) : (
<AlertTriangle
size={16}
className={tableStyles.sslWarn}
aria-label="SSL not enabled"
/>
)}
<span className={tableStyles.subText}>
{domain.sslEnabled ? 'Enabled' : 'Disabled'}
</span>
</div>
</td>
<td className={`${tableStyles.td} ${tableStyles.tdActions}`}>
<div className={tableStyles.rowActions}>
<span className={tableStyles.toggleInActions}>
<ToggleSwitch
checked={domain.isActive}
disabled={togglingActiveId === domain.id}
ariaLabel={`${domain.isActive ? 'Disable' : 'Enable'} ${domain.host}`}
onChange={(isActive) => void handleToggleActive(domain, isActive)}
/>
</span>
<button
type="button"
className={tableStyles.controlBtn}
onClick={() => void handleToggleSsl(domain, !domain.sslEnabled)}
disabled={togglingSslId === domain.id}
title={domain.sslEnabled ? 'Disable SSL' : 'Enable SSL'}
aria-label={domain.sslEnabled ? 'Disable SSL' : 'Enable SSL'}
>
{domain.sslEnabled ? <Lock size={16} /> : <Unlock size={16} />}
</button>
<button
type="button"
className={tableStyles.controlBtn}
onClick={() => openEdit(domain)}
title="Edit"
aria-label="Edit"
>
<Pencil size={16} />
</button>
<button
type="button"
className={`${tableStyles.controlBtn} ${tableStyles.controlBtnDanger}`}
onClick={() => setRemoveTarget(domain)}
title="Remove"
aria-label="Remove"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className={tableStyles.pagination}>
<div>
Page {page} / {totalPages}
</div>
<div className={tableStyles.pagerBtns}>
<button
type="button"
className={tableStyles.pageBtn}
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1 || loading}
>
Prev
</button>
{pageNumbers.map((n) => (
<button
key={n}
type="button"
className={`${tableStyles.pageBtn} ${n === page ? tableStyles.pageBtnActive : ''}`}
onClick={() => setPage(n)}
disabled={loading}
>
{n}
</button>
))}
<button
type="button"
className={tableStyles.pageBtn}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages || loading}
>
Next
</button>
</div>
</div>
</div>
</div>
<Modal
open={editOpen}
title="Edit domain"
onClose={() => {
setEditOpen(false)
setEditDomain(null)
}}
>
<div className={tableStyles.field}>
<label htmlFor="edit-domain-host">Domain name</label>
<input
id="edit-domain-host"
value={editHost}
onChange={(e) => setEditHost(e.target.value)}
disabled={editSubmitting}
/>
</div>
<div style={{ height: 10 }} />
<div className={tableStyles.field}>
<label htmlFor="edit-domain-expires">Expiry date</label>
<input
id="edit-domain-expires"
type="date"
value={editExpiresAt}
onChange={(e) => setEditExpiresAt(e.target.value)}
disabled={editSubmitting}
/>
</div>
<div style={{ height: 12 }} />
<div className={tableStyles.actionsRow}>
<button
type="button"
className={`${tableStyles.btn} ${tableStyles.btnGhost}`}
onClick={() => setEditOpen(false)}
disabled={editSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${tableStyles.btn} ${tableStyles.btnPrimary}`}
onClick={() => void submitEdit()}
disabled={editSubmitting || !editHost.trim()}
>
Save
</button>
</div>
</Modal>
<ConfirmDeleteModal
open={removeTarget !== null}
title="Remove domain?"
message={
removeTarget
? `Are you sure you want to remove "${removeTarget.host}"? This action cannot be undone.`
: ''
}
onCancel={() => setRemoveTarget(null)}
onConfirm={() => void confirmRemove()}
/>
</main>
)
}