Polish business FA layout and wire special-group keys plus SSL sync.
RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
66004a0fba
commit
672091d1f5
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { RotateCcw, Search } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { ContactSubmissionDetailModal } from '../components/ContactSubmissionDetailModal'
|
||||
import { Pagination } from '../components/Pagination'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError, isAbortError } from '../lib/api'
|
||||
import { formatCellForDisplay } from '../lib/cellNumber'
|
||||
import {
|
||||
listContactSubmissions,
|
||||
} from '../services/contactSubmissionService'
|
||||
import { listContactSubmissions } from '../services/contactSubmissionService'
|
||||
import type {
|
||||
ContactSubmission,
|
||||
ContactSubmissionsListResponse,
|
||||
@@ -19,16 +19,19 @@ import styles from './WebsiteContactPage.module.css'
|
||||
const PAGE_SIZE = 20
|
||||
const COLUMN_COUNT = 6
|
||||
|
||||
function formatDateTime(value: string) {
|
||||
function formatDateTime(value: string, locale: 'en' | 'fa') {
|
||||
const d = new Date(value)
|
||||
if (Number.isNaN(d.getTime())) return { date: value, time: '' }
|
||||
const intlLocale = locale === 'fa' ? 'fa-IR' : 'en-US'
|
||||
return {
|
||||
date: d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' }),
|
||||
time: d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
|
||||
date: d.toLocaleDateString(intlLocale, { year: 'numeric', month: 'short', day: '2-digit' }),
|
||||
time: d.toLocaleTimeString(intlLocale, { hour: '2-digit', minute: '2-digit' }),
|
||||
}
|
||||
}
|
||||
|
||||
export function WebsiteContactPage() {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const [data, setData] = useState<ContactSubmissionsListResponse | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -58,7 +61,7 @@ export function WebsiteContactPage() {
|
||||
setData(result)
|
||||
} catch (err) {
|
||||
if (isAbortError(err) || controller.signal.aborted) return
|
||||
setError(err instanceof ApiError ? err.message : 'Unable to load contact submissions.')
|
||||
setError(err instanceof ApiError ? err.message : t('website.contact.errorLoad'))
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false)
|
||||
}
|
||||
@@ -109,24 +112,22 @@ export function WebsiteContactPage() {
|
||||
|
||||
<div className={pageStyles.pageHeader}>
|
||||
<div>
|
||||
<h2 className={pageStyles.pageTitle}>Contact us form</h2>
|
||||
<p className={pageStyles.pageSubtitle}>
|
||||
Submissions received from your website contact form.
|
||||
</p>
|
||||
<h2 className={pageStyles.pageTitle}>{t('title.contactForm')}</h2>
|
||||
<p className={pageStyles.pageSubtitle}>{t('website.contact.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={filterStyles.filtersPanel}>
|
||||
<div className={filterStyles.filtersTitle}>Filters</div>
|
||||
<div className={filterStyles.filtersTitle}>{t('website.contact.filters')}</div>
|
||||
<div className={filterStyles.filtersGrid}>
|
||||
<div className={filterStyles.filtersInputs}>
|
||||
<div className={`${filterStyles.field} ${filterStyles.fieldCol4}`}>
|
||||
<label htmlFor="filter-contact-q">Search</label>
|
||||
<input
|
||||
id="filter-contact-q"
|
||||
value={draftQuery}
|
||||
onChange={(e) => setDraftQuery(e.target.value)}
|
||||
placeholder="Title, name, email, cell number, or message"
|
||||
placeholder={t('website.contact.searchPlaceholder')}
|
||||
aria-label={t('website.contact.search')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') applyFilters()
|
||||
}}
|
||||
@@ -139,8 +140,8 @@ export function WebsiteContactPage() {
|
||||
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnPrimary}`}
|
||||
onClick={applyFilters}
|
||||
disabled={loading}
|
||||
aria-label="Search"
|
||||
title="Search"
|
||||
aria-label={t('website.contact.search')}
|
||||
title={t('website.contact.search')}
|
||||
>
|
||||
<Search size={18} />
|
||||
</button>
|
||||
@@ -149,8 +150,8 @@ export function WebsiteContactPage() {
|
||||
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnGhost}`}
|
||||
onClick={clearFilters}
|
||||
disabled={loading}
|
||||
aria-label="Clear filters"
|
||||
title="Clear filters"
|
||||
aria-label={t('website.contact.clearFilters')}
|
||||
title={t('website.contact.clearFilters')}
|
||||
>
|
||||
<RotateCcw size={18} />
|
||||
</button>
|
||||
@@ -161,15 +162,19 @@ export function WebsiteContactPage() {
|
||||
<div className={styles.tablePanel}>
|
||||
<div className={styles.tableWrap}>
|
||||
<div className={styles.tableHeader}>
|
||||
<div className={styles.tableHeaderTitle}>Submissions</div>
|
||||
<div className={styles.tableHeaderTitle}>{t('website.contact.listTitle')}</div>
|
||||
<div className={styles.meta}>
|
||||
{data ? (
|
||||
data.total > 0 ? (
|
||||
<>
|
||||
Showing {showingFrom} - {showingTo} of {data.total}
|
||||
{t('website.contact.showing', {
|
||||
from: showingFrom,
|
||||
to: showingTo,
|
||||
total: data.total,
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
'No submissions'
|
||||
t('website.contact.none')
|
||||
)
|
||||
) : (
|
||||
' '
|
||||
@@ -183,19 +188,19 @@ export function WebsiteContactPage() {
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.th}>Title</th>
|
||||
<th className={styles.th}>Name</th>
|
||||
<th className={styles.th}>Email</th>
|
||||
<th className={styles.th}>Cell number</th>
|
||||
<th className={styles.th}>Date</th>
|
||||
<th className={styles.th}>Time</th>
|
||||
<th className={styles.th}>{t('website.contact.col.title')}</th>
|
||||
<th className={styles.th}>{t('website.contact.col.name')}</th>
|
||||
<th className={styles.th}>{t('website.contact.col.email')}</th>
|
||||
<th className={styles.th}>{t('website.contact.col.cell')}</th>
|
||||
<th className={styles.th}>{t('website.contact.col.date')}</th>
|
||||
<th className={styles.th}>{t('website.contact.col.time')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && (
|
||||
<tr>
|
||||
<td className={styles.td} colSpan={COLUMN_COUNT}>
|
||||
Loading...
|
||||
{t('website.contact.loading')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -203,14 +208,14 @@ export function WebsiteContactPage() {
|
||||
{!loading && data?.items.length === 0 && (
|
||||
<tr>
|
||||
<td className={styles.td} colSpan={COLUMN_COUNT}>
|
||||
No submissions found.
|
||||
{t('website.contact.empty')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
data?.items.map((item) => {
|
||||
const { date, time } = formatDateTime(item.createdAt)
|
||||
const { date, time } = formatDateTime(item.createdAt, locale)
|
||||
return (
|
||||
<tr
|
||||
key={item.id}
|
||||
@@ -224,7 +229,7 @@ export function WebsiteContactPage() {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`View submission from ${item.name}`}
|
||||
aria-label={t('website.contact.viewSubmission', { name: item.name })}
|
||||
>
|
||||
<td className={styles.td}>{item.title}</td>
|
||||
<td className={styles.td}>{item.name}</td>
|
||||
@@ -243,7 +248,12 @@ export function WebsiteContactPage() {
|
||||
|
||||
<div className={styles.pagination}>
|
||||
<div>
|
||||
Page {page} / {totalPages} · {PAGE_SIZE} per page · {data?.total ?? 0} total
|
||||
{t('website.contact.pageMeta', {
|
||||
page,
|
||||
totalPages,
|
||||
pageSize: PAGE_SIZE,
|
||||
total: data?.total ?? 0,
|
||||
})}
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
|
||||
Reference in New Issue
Block a user