mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Improve portfolio title images, branding, and super-admin domains UI.
Add aspect-ratio crop presets for portfolio title images, fixed card image placeholders with contained centering, Meshkee favicon/logo fallbacks, login branding updates, category FAB alignment, and super-admin website/domain SSL tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f566387c61
commit
db52a83f98
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
Regular → Executable
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 12 KiB |
@@ -287,6 +287,14 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sslIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 0;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.sslOk {
|
||||
color: #16a34a;
|
||||
flex-shrink: 0;
|
||||
@@ -319,12 +327,18 @@
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.controlBtn:hover {
|
||||
.controlBtn:hover:not(:disabled) {
|
||||
background: rgba(var(--primary-rgb) / 0.1);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.controlBtnDanger:hover {
|
||||
.controlBtn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.controlBtnDanger:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
@@ -601,13 +601,25 @@ export function BusinessesPage() {
|
||||
<div className={styles.domainCell}>
|
||||
<span>{b.domain}</span>
|
||||
{b.sslEnabled ? (
|
||||
<Lock size={16} className={styles.sslOk} aria-label="SSL enabled" />
|
||||
<span
|
||||
className={styles.sslIcon}
|
||||
title="SSL enabled"
|
||||
aria-label="SSL enabled"
|
||||
>
|
||||
<Lock size={16} className={styles.sslOk} aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
className={styles.sslWarn}
|
||||
<span
|
||||
className={styles.sslIcon}
|
||||
title="SSL not enabled"
|
||||
aria-label="SSL not enabled"
|
||||
/>
|
||||
>
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
className={styles.sslWarn}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -16,3 +16,41 @@
|
||||
.inactiveRow {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.spin {
|
||||
display: block;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.deployBusy {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.deployCell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
.deployStatus {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.deployOk {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.deployFail {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { AlertTriangle, Lock, Pencil, RotateCcw, Search, Trash2, Unlock } from 'lucide-react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Loader2,
|
||||
Lock,
|
||||
Pencil,
|
||||
Rocket,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Trash2,
|
||||
Unlock,
|
||||
} from 'lucide-react'
|
||||
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
||||
import { Modal } from '../components/Modal'
|
||||
import { ToggleSwitch } from '../components/ToggleSwitch'
|
||||
@@ -7,6 +18,7 @@ import type { DomainListItem, DomainsListResponse } from '../types/domain'
|
||||
import type { ListDomainsParams } from '../services/domainService'
|
||||
import { ApiError, isAbortError } from '../lib/api'
|
||||
import {
|
||||
deployDomain,
|
||||
listDomains,
|
||||
removeDomain,
|
||||
setDomainActive,
|
||||
@@ -51,6 +63,31 @@ function daysLeftClass(expiresAt: string | null) {
|
||||
return styles.daysOk
|
||||
}
|
||||
|
||||
function formatDeployAt(iso: string | null) {
|
||||
if (!iso) return '—'
|
||||
const d = new Date(iso)
|
||||
if (Number.isNaN(d.getTime())) return '—'
|
||||
return d.toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function deployStatusLabel(status: DomainListItem['lastDeployStatus']) {
|
||||
if (status === 'started') return 'Started'
|
||||
if (status === 'failed') return 'Failed'
|
||||
return null
|
||||
}
|
||||
|
||||
function deployStatusClass(status: DomainListItem['lastDeployStatus']) {
|
||||
if (status === 'started') return styles.deployOk
|
||||
if (status === 'failed') return styles.deployFail
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function WebsitesPage() {
|
||||
const { showToast } = useToast()
|
||||
const [data, setData] = useState<DomainsListResponse | null>(null)
|
||||
@@ -70,6 +107,7 @@ export function WebsitesPage() {
|
||||
const [removeTarget, setRemoveTarget] = useState<DomainListItem | null>(null)
|
||||
const [togglingActiveId, setTogglingActiveId] = useState<number | null>(null)
|
||||
const [togglingSslId, setTogglingSslId] = useState<number | null>(null)
|
||||
const [deployingId, setDeployingId] = useState<DomainListItem['id'] | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
@@ -229,6 +267,59 @@ export function WebsitesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeploy(domain: DomainListItem) {
|
||||
if (!domain.deploySlug) return
|
||||
if (deployingId != null) return
|
||||
|
||||
// Paint spinner + disabled state before the network call starts
|
||||
flushSync(() => {
|
||||
setDeployingId(domain.id)
|
||||
})
|
||||
setError('')
|
||||
showToast(`Deploying "${domain.host}"…`, 'info')
|
||||
|
||||
try {
|
||||
const result = await deployDomain(domain.id)
|
||||
setData((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
items: prev.items.map((item) =>
|
||||
item.id === domain.id
|
||||
? {
|
||||
...item,
|
||||
lastDeployedAt: result.lastDeployedAt,
|
||||
lastDeployStatus: result.lastDeployStatus,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
}
|
||||
})
|
||||
showToast(`Deploy started for "${result.host}".`, 'success')
|
||||
} catch (err) {
|
||||
const message = err instanceof ApiError ? err.message : 'Deploy failed to start.'
|
||||
setError(message)
|
||||
setData((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
items: prev.items.map((item) =>
|
||||
item.id === domain.id
|
||||
? {
|
||||
...item,
|
||||
lastDeployedAt: new Date().toISOString(),
|
||||
lastDeployStatus: 'failed',
|
||||
}
|
||||
: item,
|
||||
),
|
||||
}
|
||||
})
|
||||
showToast(message, 'error')
|
||||
} finally {
|
||||
setDeployingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRemove() {
|
||||
if (!removeTarget) return
|
||||
setError('')
|
||||
@@ -330,13 +421,14 @@ export function WebsitesPage() {
|
||||
<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}>Latest deploy</th>
|
||||
<th className={`${tableStyles.th} ${tableStyles.thActions}`}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && (
|
||||
<tr>
|
||||
<td className={tableStyles.td} colSpan={5}>
|
||||
<td className={tableStyles.td} colSpan={6}>
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
@@ -344,7 +436,7 @@ export function WebsitesPage() {
|
||||
|
||||
{!loading && data?.items?.length === 0 && (
|
||||
<tr>
|
||||
<td className={tableStyles.td} colSpan={5}>
|
||||
<td className={tableStyles.td} colSpan={6}>
|
||||
No results found.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -366,19 +458,49 @@ export function WebsitesPage() {
|
||||
<td className={tableStyles.td}>
|
||||
<div className={tableStyles.domainCell}>
|
||||
{domain.sslEnabled ? (
|
||||
<Lock size={16} className={tableStyles.sslOk} aria-label="SSL enabled" />
|
||||
<span
|
||||
className={tableStyles.sslIcon}
|
||||
title="SSL enabled"
|
||||
aria-label="SSL enabled"
|
||||
>
|
||||
<Lock size={16} className={tableStyles.sslOk} aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
className={tableStyles.sslWarn}
|
||||
<span
|
||||
className={tableStyles.sslIcon}
|
||||
title="SSL not enabled"
|
||||
aria-label="SSL not enabled"
|
||||
/>
|
||||
>
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
className={tableStyles.sslWarn}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className={tableStyles.subText}>
|
||||
{domain.sslEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className={tableStyles.td}>
|
||||
{domain.lastDeployedAt ? (
|
||||
<div className={styles.deployCell}>
|
||||
<span className={tableStyles.subText}>
|
||||
{formatDeployAt(domain.lastDeployedAt)}
|
||||
</span>
|
||||
{deployStatusLabel(domain.lastDeployStatus) ? (
|
||||
<span
|
||||
className={`${styles.deployStatus} ${deployStatusClass(domain.lastDeployStatus) ?? ''}`}
|
||||
>
|
||||
{deployStatusLabel(domain.lastDeployStatus)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span className={tableStyles.subText}>—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={`${tableStyles.td} ${tableStyles.tdActions}`}>
|
||||
<div className={tableStyles.rowActions}>
|
||||
<span className={tableStyles.toggleInActions}>
|
||||
@@ -389,6 +511,32 @@ export function WebsitesPage() {
|
||||
onChange={(isActive) => void handleToggleActive(domain, isActive)}
|
||||
/>
|
||||
</span>
|
||||
{domain.deploySlug ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${tableStyles.controlBtn} ${
|
||||
deployingId === domain.id ? styles.deployBusy : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (deployingId != null) return
|
||||
void handleDeploy(domain)
|
||||
}}
|
||||
disabled={deployingId === domain.id}
|
||||
title={
|
||||
deployingId === domain.id ? 'Deploying…' : 'Deploy website'
|
||||
}
|
||||
aria-label={
|
||||
deployingId === domain.id ? 'Deploying' : 'Deploy website'
|
||||
}
|
||||
aria-busy={deployingId === domain.id}
|
||||
>
|
||||
{deployingId === domain.id ? (
|
||||
<Loader2 size={16} className={styles.spin} aria-hidden />
|
||||
) : (
|
||||
<Rocket size={16} />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={tableStyles.controlBtn}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import type { DomainsListResponse } from '../types/domain'
|
||||
import type { DeployDomainResponse, DomainsListResponse } from '../types/domain'
|
||||
|
||||
export interface ListDomainsParams {
|
||||
page?: number
|
||||
@@ -53,3 +53,10 @@ export async function removeDomain(domainId: number | string) {
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deployDomain(domainId: number | string) {
|
||||
return apiRequest<DeployDomainResponse>(`/domains/${domainId}/deploy`, {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type DomainDeployStatus = 'started' | 'failed'
|
||||
|
||||
export interface DomainListItem {
|
||||
id: number
|
||||
host: string
|
||||
@@ -7,6 +9,10 @@ export interface DomainListItem {
|
||||
isActive: boolean
|
||||
expiresAt: string | null
|
||||
createdAt: string
|
||||
/** Present when this apex has a storefront on the websites VM */
|
||||
deploySlug: string | null
|
||||
lastDeployedAt: string | null
|
||||
lastDeployStatus: DomainDeployStatus | null
|
||||
}
|
||||
|
||||
export interface DomainsListResponse {
|
||||
@@ -15,3 +21,12 @@ export interface DomainsListResponse {
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface DeployDomainResponse {
|
||||
status: 'accepted'
|
||||
slug: string
|
||||
host: string
|
||||
message: string
|
||||
lastDeployedAt: string | null
|
||||
lastDeployStatus: DomainDeployStatus | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user