Add Website Tags & Badges management for custom meta and trust seals.
Business admins can manage head meta tags and eNamad/Samandehi footer HTML from the dashboard without hardcoding them in each storefront. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
cb20fe3c40
commit
68b985dee3
@@ -0,0 +1,171 @@
|
|||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--modal-overlay-bg);
|
||||||
|
backdrop-filter: blur(var(--modal-overlay-blur, 16px));
|
||||||
|
-webkit-backdrop-filter: blur(var(--modal-overlay-blur, 16px));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 260;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
background: var(--surface);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 20px 60px rgba(31, 38, 135, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn:hover {
|
||||||
|
background: rgba(var(--primary-rgb) / 0.1);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
color: #b91c1c;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input,
|
||||||
|
.field textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--field-height);
|
||||||
|
padding: var(--field-padding-y) var(--field-padding-x);
|
||||||
|
font-size: var(--field-font-size);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input:focus,
|
||||||
|
.field textarea:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueField {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelBtn {
|
||||||
|
min-height: var(--field-height);
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelBtn:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn {
|
||||||
|
min-height: var(--field-height);
|
||||||
|
padding: 0 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: 0 4px 12px rgba(var(--primary-rgb) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
|
||||||
|
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
|
||||||
|
.modalIn { animation: modalFadeIn 0.25s ease forwards; }
|
||||||
|
.modalOut { animation: modalFadeOut 0.22s ease forwards; }
|
||||||
|
|
||||||
|
@keyframes overlayFadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes overlayFadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
@keyframes modalFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(12px) scale(0.98); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes modalFadeOut {
|
||||||
|
from { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
to { opacity: 0; transform: translateY(8px) scale(0.98); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import { useLocale } from '@meshkee/dashboard-ui'
|
||||||
|
import { useT } from '../i18n/useT'
|
||||||
|
import styles from './WebsiteMetaTagModal.module.css'
|
||||||
|
|
||||||
|
const ANIMATION_MS = 220
|
||||||
|
const META_NAME_RE = /^[a-zA-Z0-9_.:-]{1,80}$/
|
||||||
|
|
||||||
|
interface WebsiteMetaTagModalProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (data: { name: string; content: string }) => void
|
||||||
|
initial?: { name: string; content: string } | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebsiteMetaTagModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
initial = null,
|
||||||
|
}: WebsiteMetaTagModalProps) {
|
||||||
|
const t = useT()
|
||||||
|
const { locale } = useLocale()
|
||||||
|
const isFa = locale === 'fa'
|
||||||
|
const isEditing = Boolean(initial)
|
||||||
|
const [mounted, setMounted] = useState(open)
|
||||||
|
const [closing, setClosing] = useState(false)
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [content, setContent] = useState('')
|
||||||
|
const [formError, setFormError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setMounted(true)
|
||||||
|
setClosing(false)
|
||||||
|
setName(initial?.name ?? '')
|
||||||
|
setContent(initial?.content ?? '')
|
||||||
|
setFormError('')
|
||||||
|
} else if (mounted) {
|
||||||
|
setClosing(true)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setMounted(false)
|
||||||
|
setClosing(false)
|
||||||
|
}, ANIMATION_MS)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
// Reset fields only when opening; ignore `initial` identity churn while open.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional
|
||||||
|
}, [open, mounted])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted || closing) return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKey)
|
||||||
|
return () => document.removeEventListener('keydown', onKey)
|
||||||
|
}, [mounted, closing, onClose])
|
||||||
|
|
||||||
|
if (!mounted) return null
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
const trimmedName = name.trim()
|
||||||
|
const trimmedContent = content.trim()
|
||||||
|
if (!trimmedName || !META_NAME_RE.test(trimmedName)) {
|
||||||
|
setFormError(t('website.badges.meta.nameInvalid'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!trimmedContent) {
|
||||||
|
setFormError(t('website.badges.meta.contentRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSubmit({ name: trimmedName, content: trimmedContent })
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className={`${styles.overlay} ${closing ? styles.overlayOut : styles.overlayIn}`}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${styles.modal} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="website-meta-tag-modal-title"
|
||||||
|
lang={isFa ? 'fa' : 'en'}
|
||||||
|
dir={isFa ? 'rtl' : 'ltr'}
|
||||||
|
>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h3 id="website-meta-tag-modal-title" className={styles.title}>
|
||||||
|
{isEditing ? t('website.badges.meta.editTitle') : t('website.badges.meta.modalTitle')}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.closeBtn}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label={t('common.close')}
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
|
{formError ? (
|
||||||
|
<div className={styles.error} role="alert">
|
||||||
|
{formError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="website-meta-tag-name">{t('website.badges.meta.nameLabel')}</label>
|
||||||
|
<input
|
||||||
|
id="website-meta-tag-name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder={t('website.badges.meta.namePlaceholder')}
|
||||||
|
dir="ltr"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`${styles.field} ${styles.valueField}`}>
|
||||||
|
<label htmlFor="website-meta-tag-value">{t('website.badges.meta.valueLabel')}</label>
|
||||||
|
<textarea
|
||||||
|
id="website-meta-tag-value"
|
||||||
|
rows={4}
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder={t('website.badges.meta.contentPlaceholder')}
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button type="button" className={styles.cancelBtn} onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button type="submit" className={styles.submitBtn}>
|
||||||
|
{isEditing ? t('website.badges.meta.saveEdit') : t('website.badges.meta.add')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ const en = {
|
|||||||
'nav.website.contact': 'Contact Us Form',
|
'nav.website.contact': 'Contact Us Form',
|
||||||
'nav.website.subscriptions': 'Subscriptions',
|
'nav.website.subscriptions': 'Subscriptions',
|
||||||
'nav.website.faq': 'FAQ',
|
'nav.website.faq': 'FAQ',
|
||||||
'nav.website.badges': 'Badges',
|
'nav.website.badges': 'Tags & Badges',
|
||||||
'nav.website.settings': 'Settings',
|
'nav.website.settings': 'Settings',
|
||||||
'nav.website.ePayment': 'E-Payment',
|
'nav.website.ePayment': 'E-Payment',
|
||||||
'nav.help': 'Help Center',
|
'nav.help': 'Help Center',
|
||||||
@@ -2015,8 +2015,9 @@ const en = {
|
|||||||
'website.card.subscriptions.link': 'Manage subscriptions',
|
'website.card.subscriptions.link': 'Manage subscriptions',
|
||||||
'website.card.faq.desc': 'Create and organize frequently asked questions for your website.',
|
'website.card.faq.desc': 'Create and organize frequently asked questions for your website.',
|
||||||
'website.card.faq.link': 'Manage FAQ',
|
'website.card.faq.link': 'Manage FAQ',
|
||||||
'website.card.badges.desc': 'Show trust badges, certifications, and highlights on your website.',
|
'website.card.badges.desc':
|
||||||
'website.card.badges.link': 'Manage badges',
|
'Manage custom meta tags and trust badges (eNamad, Samandehi, and more) for your website.',
|
||||||
|
'website.card.badges.link': 'Manage tags & badges',
|
||||||
'website.card.settings.desc': 'Choose how special products are sourced for the website.',
|
'website.card.settings.desc': 'Choose how special products are sourced for the website.',
|
||||||
'website.card.settings.link': 'Manage settings',
|
'website.card.settings.link': 'Manage settings',
|
||||||
'settings.subtitle': 'Manage SMS balance and message templates.',
|
'settings.subtitle': 'Manage SMS balance and message templates.',
|
||||||
@@ -2427,13 +2428,36 @@ const en = {
|
|||||||
'website.faq.feature.groups': 'Group items by category',
|
'website.faq.feature.groups': 'Group items by category',
|
||||||
'website.faq.feature.publish': 'Publish or hide individual entries',
|
'website.faq.feature.publish': 'Publish or hide individual entries',
|
||||||
|
|
||||||
'website.badges.subtitle': 'Show trust badges and highlights on your public website.',
|
'website.badges.subtitle': 'Manage custom meta tags and trust badges for your public website.',
|
||||||
'website.badges.lead':
|
'website.badges.save': 'Save tags & badges',
|
||||||
'Display certifications, guarantees, and trust signals to website visitors.',
|
'website.badges.saving': 'Saving…',
|
||||||
'website.badges.note': 'Badge management will be connected here. Planned options include:',
|
'website.badges.saved': 'Tags & badges saved.',
|
||||||
'website.badges.feature.upload': 'Upload badge images with title and link',
|
'website.badges.errorLoad': 'Unable to load tags & badges.',
|
||||||
'website.badges.feature.reorder': 'Reorder badges for homepage or footer display',
|
'website.badges.errorSave': 'Unable to save tags & badges.',
|
||||||
'website.badges.feature.toggle': 'Toggle visibility per badge',
|
'website.badges.meta.title': 'Custom meta tags',
|
||||||
|
'website.badges.meta.desc': 'Each row is added to the website head as',
|
||||||
|
'website.badges.meta.sample': '<meta name="…" content="…">',
|
||||||
|
'website.badges.meta.add': 'Add meta tag',
|
||||||
|
'website.badges.meta.modalTitle': 'Add meta tag',
|
||||||
|
'website.badges.meta.editTitle': 'Edit meta tag',
|
||||||
|
'website.badges.meta.saveEdit': 'Save changes',
|
||||||
|
'website.badges.meta.edit': 'Edit meta tag',
|
||||||
|
'website.badges.meta.nameLabel': 'Name',
|
||||||
|
'website.badges.meta.valueLabel': 'Value',
|
||||||
|
'website.badges.meta.remove': 'Remove meta tag',
|
||||||
|
'website.badges.meta.empty': 'No custom tags yet. Use + to add a name and value.',
|
||||||
|
'website.badges.meta.namePlaceholder': 'name (e.g. google-site-verification)',
|
||||||
|
'website.badges.meta.contentPlaceholder': 'value / content',
|
||||||
|
'website.badges.meta.nameInvalid':
|
||||||
|
'Meta tag names may only use letters, numbers, and _ . : -',
|
||||||
|
'website.badges.meta.contentRequired': 'Each meta tag needs both a name and a value.',
|
||||||
|
'website.badges.enamad.title': 'eNamad',
|
||||||
|
'website.badges.enamad.desc':
|
||||||
|
'If you have received an eNamad badge for your website, enter its HTML code below to show it on the site.',
|
||||||
|
'website.badges.samandehi.title': 'Samandehi',
|
||||||
|
'website.badges.samandehi.desc':
|
||||||
|
'If you have received a Samandehi badge for your website, enter its HTML code below to show it on the site.',
|
||||||
|
'website.badges.trust.embedPlaceholder': '<a …><img …></a>',
|
||||||
|
|
||||||
'website.ePayment.subtitle': 'Configure online payment methods for your website checkout.',
|
'website.ePayment.subtitle': 'Configure online payment methods for your website checkout.',
|
||||||
'website.ePayment.master': 'Enable online payments',
|
'website.ePayment.master': 'Enable online payments',
|
||||||
@@ -2530,7 +2554,7 @@ const en = {
|
|||||||
'title.contactForm': 'Contact Us Form',
|
'title.contactForm': 'Contact Us Form',
|
||||||
'title.subscriptions': 'Subscriptions',
|
'title.subscriptions': 'Subscriptions',
|
||||||
'title.faq': 'FAQ',
|
'title.faq': 'FAQ',
|
||||||
'title.badges': 'Badges',
|
'title.badges': 'Tags & Badges',
|
||||||
'title.ePayment': 'E-Payment',
|
'title.ePayment': 'E-Payment',
|
||||||
'title.invoices': 'Invoices',
|
'title.invoices': 'Invoices',
|
||||||
'title.finance': 'Finance Management',
|
'title.finance': 'Finance Management',
|
||||||
@@ -2987,7 +3011,7 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'nav.website.contact': 'فرم تماس با ما',
|
'nav.website.contact': 'فرم تماس با ما',
|
||||||
'nav.website.subscriptions': 'عضویتها',
|
'nav.website.subscriptions': 'عضویتها',
|
||||||
'nav.website.faq': 'سوالات متداول',
|
'nav.website.faq': 'سوالات متداول',
|
||||||
'nav.website.badges': 'نشانها',
|
'nav.website.badges': 'تگ و نشانها',
|
||||||
'nav.website.settings': 'تنظیمات',
|
'nav.website.settings': 'تنظیمات',
|
||||||
'nav.website.ePayment': 'پرداخت الکترونیک',
|
'nav.website.ePayment': 'پرداخت الکترونیک',
|
||||||
'nav.help': 'مرکز راهنما',
|
'nav.help': 'مرکز راهنما',
|
||||||
@@ -4910,8 +4934,9 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'website.card.subscriptions.link': 'مدیریت عضویتها',
|
'website.card.subscriptions.link': 'مدیریت عضویتها',
|
||||||
'website.card.faq.desc': 'سوالات متداول وبسایت را ایجاد و سازماندهی کنید.',
|
'website.card.faq.desc': 'سوالات متداول وبسایت را ایجاد و سازماندهی کنید.',
|
||||||
'website.card.faq.link': 'مدیریت سوالات متداول',
|
'website.card.faq.link': 'مدیریت سوالات متداول',
|
||||||
'website.card.badges.desc': 'نشانهای اعتماد، گواهیها و برجستهها را در وبسایت نمایش دهید.',
|
'website.card.badges.desc':
|
||||||
'website.card.badges.link': 'مدیریت نشانها',
|
'تگهای متا سفارشی و نشانهای اعتماد (اینماد، ساماندهی و …) را برای وبسایت مدیریت کنید.',
|
||||||
|
'website.card.badges.link': 'مدیریت تگ و نشانها',
|
||||||
'website.card.settings.desc': 'منبع خواندن محصولات ویژه وبسایت را انتخاب کنید.',
|
'website.card.settings.desc': 'منبع خواندن محصولات ویژه وبسایت را انتخاب کنید.',
|
||||||
'website.card.settings.link': 'مدیریت تنظیمات',
|
'website.card.settings.link': 'مدیریت تنظیمات',
|
||||||
'settings.subtitle': 'اعتبار پیامک و قالبهای پیام را مدیریت کنید.',
|
'settings.subtitle': 'اعتبار پیامک و قالبهای پیام را مدیریت کنید.',
|
||||||
@@ -5321,12 +5346,36 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'website.faq.feature.groups': 'گروهبندی موارد بر اساس دسته',
|
'website.faq.feature.groups': 'گروهبندی موارد بر اساس دسته',
|
||||||
'website.faq.feature.publish': 'انتشار یا مخفی کردن هر مورد',
|
'website.faq.feature.publish': 'انتشار یا مخفی کردن هر مورد',
|
||||||
|
|
||||||
'website.badges.subtitle': 'نشانهای اعتماد و برجستهها را در وبسایت عمومی نمایش دهید.',
|
'website.badges.subtitle': 'تگهای متا سفارشی و نشانهای اعتماد را برای وبسایت عمومی مدیریت کنید.',
|
||||||
'website.badges.lead': 'گواهیها، تضمینها و نشانههای اعتماد را به بازدیدکنندگان نشان دهید.',
|
'website.badges.save': 'ذخیره تگ و نشانها',
|
||||||
'website.badges.note': 'مدیریت نشانها بهزودی اینجا متصل میشود. گزینههای برنامهریزیشده:',
|
'website.badges.saving': 'در حال ذخیره…',
|
||||||
'website.badges.feature.upload': 'آپلود تصویر نشان با عنوان و لینک',
|
'website.badges.saved': 'تگ و نشانها ذخیره شد.',
|
||||||
'website.badges.feature.reorder': 'مرتبسازی نشانها برای صفحه اصلی یا پاورقی',
|
'website.badges.errorLoad': 'بارگذاری تگ و نشانها ممکن نشد.',
|
||||||
'website.badges.feature.toggle': 'نمایش یا مخفی کردن هر نشان',
|
'website.badges.errorSave': 'ذخیره تگ و نشانها ممکن نشد.',
|
||||||
|
'website.badges.meta.title': 'تگهای متا سفارشی',
|
||||||
|
'website.badges.meta.desc': 'هر ردیف در هد وبسایت قرار میگیرد بهصورت',
|
||||||
|
'website.badges.meta.sample': '<meta name="…" content="…">',
|
||||||
|
'website.badges.meta.add': 'افزودن تگ متا',
|
||||||
|
'website.badges.meta.modalTitle': 'افزودن تگ متا',
|
||||||
|
'website.badges.meta.editTitle': 'ویرایش تگ متا',
|
||||||
|
'website.badges.meta.saveEdit': 'ذخیره تغییرات',
|
||||||
|
'website.badges.meta.edit': 'ویرایش تگ متا',
|
||||||
|
'website.badges.meta.nameLabel': 'نام',
|
||||||
|
'website.badges.meta.valueLabel': 'مقدار',
|
||||||
|
'website.badges.meta.remove': 'حذف تگ متا',
|
||||||
|
'website.badges.meta.empty': 'هنوز تگ سفارشی ندارید. با + نام و مقدار اضافه کنید.',
|
||||||
|
'website.badges.meta.namePlaceholder': 'نام (مثلاً google-site-verification)',
|
||||||
|
'website.badges.meta.contentPlaceholder': 'مقدار / محتوا',
|
||||||
|
'website.badges.meta.nameInvalid':
|
||||||
|
'نام تگ فقط میتواند شامل حروف، اعداد و _ . : - باشد.',
|
||||||
|
'website.badges.meta.contentRequired': 'هر تگ متا باید هم نام و هم مقدار داشته باشد.',
|
||||||
|
'website.badges.enamad.title': 'اینماد',
|
||||||
|
'website.badges.enamad.desc':
|
||||||
|
'در صورتی که نشان اینماد برای وبسایت خود دریافت کرده اید، کد HTML آن را در زیر وارد کنید تا در وبسایت نمایش داده شود.',
|
||||||
|
'website.badges.samandehi.title': 'ساماندهی',
|
||||||
|
'website.badges.samandehi.desc':
|
||||||
|
'در صورتی که نشان سامان دهی برای وبسایت خود دریافت کرده اید، کد HTML آن را در زیر وارد کنید تا در وبسایت نمایش داده شود.',
|
||||||
|
'website.badges.trust.embedPlaceholder': '<a …><img …></a>',
|
||||||
|
|
||||||
'website.ePayment.subtitle': 'روشهای پرداخت آنلاین برای تسویه حساب وبسایت را پیکربندی کنید.',
|
'website.ePayment.subtitle': 'روشهای پرداخت آنلاین برای تسویه حساب وبسایت را پیکربندی کنید.',
|
||||||
'website.ePayment.master': 'فعالسازی پرداخت آنلاین',
|
'website.ePayment.master': 'فعالسازی پرداخت آنلاین',
|
||||||
@@ -5426,7 +5475,7 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'title.contactForm': 'فرم تماس با ما',
|
'title.contactForm': 'فرم تماس با ما',
|
||||||
'title.subscriptions': 'عضویتها',
|
'title.subscriptions': 'عضویتها',
|
||||||
'title.faq': 'سوالات متداول',
|
'title.faq': 'سوالات متداول',
|
||||||
'title.badges': 'نشانها',
|
'title.badges': 'تگ و نشانها',
|
||||||
'title.ePayment': 'پرداخت الکترونیک',
|
'title.ePayment': 'پرداخت الکترونیک',
|
||||||
'title.invoices': 'فاکتورها',
|
'title.invoices': 'فاکتورها',
|
||||||
'title.finance': 'مدیریت مالی',
|
'title.finance': 'مدیریت مالی',
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
.emptyHint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaDesc {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oneLineHint {
|
||||||
|
max-width: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaSample {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||||
|
border-radius: 6px;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaList {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(148, 163, 184, 0.08);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaName {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 220px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: rgba(15, 23, 42, 0.45);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaValue {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaActions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.metaRow {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
grid-template-areas:
|
||||||
|
'name actions'
|
||||||
|
'value actions';
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaName {
|
||||||
|
grid-area: name;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaValue {
|
||||||
|
grid-area: value;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaActions {
|
||||||
|
grid-area: actions;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,374 @@
|
|||||||
import { WebsiteSectionShell } from '../components/WebsiteSectionShell'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Pencil, Plus, Trash2 } from 'lucide-react'
|
||||||
|
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||||
|
import { Tooltip } from '../components/Tooltip'
|
||||||
|
import { WebsiteMetaTagModal } from '../components/WebsiteMetaTagModal'
|
||||||
|
import { useToast } from '../context/ToastContext'
|
||||||
|
import type { BusinessMessageKey } from '../i18n/messages'
|
||||||
import { useT } from '../i18n/useT'
|
import { useT } from '../i18n/useT'
|
||||||
import styles from './WebsitePage.module.css'
|
import { ApiError } from '../lib/api'
|
||||||
|
import {
|
||||||
|
DEFAULT_WEBSITE_TAGS_AND_BADGES,
|
||||||
|
DEFAULT_WEBSITE_TRUST_BADGE,
|
||||||
|
getSettings,
|
||||||
|
updateWebsiteSettings,
|
||||||
|
type WebsiteMetaTag,
|
||||||
|
type WebsiteTagsAndBadgesSettings,
|
||||||
|
type WebsiteTrustBadgeSettings,
|
||||||
|
} from '../services/settingsService'
|
||||||
|
import controlStyles from '../components/CategoryRow.module.css'
|
||||||
|
import { createId } from '../utils/id'
|
||||||
|
import pageStyles from '../components/PageContent.module.css'
|
||||||
|
import sharedStyles from './ProductSettingsPage.module.css'
|
||||||
|
import settingsStyles from './WebsiteSettingsPage.module.css'
|
||||||
|
import styles from './WebsiteBadgesPage.module.css'
|
||||||
|
|
||||||
|
const META_NAME_RE = /^[a-zA-Z0-9_.:-]{1,80}$/
|
||||||
|
const MAX_META_TAGS = 40
|
||||||
|
|
||||||
|
type TrustKind = 'enamad' | 'samandehi'
|
||||||
|
|
||||||
|
function normalizeDraft(
|
||||||
|
value: WebsiteTagsAndBadgesSettings | null | undefined,
|
||||||
|
): WebsiteTagsAndBadgesSettings {
|
||||||
|
if (!value) {
|
||||||
|
return {
|
||||||
|
metaTags: [],
|
||||||
|
enamad: { ...DEFAULT_WEBSITE_TRUST_BADGE },
|
||||||
|
samandehi: { ...DEFAULT_WEBSITE_TRUST_BADGE },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
metaTags: Array.isArray(value.metaTags)
|
||||||
|
? value.metaTags.map((tag) => ({
|
||||||
|
id: tag.id || createId(),
|
||||||
|
name: tag.name ?? '',
|
||||||
|
content: tag.content ?? '',
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
enamad: { ...DEFAULT_WEBSITE_TRUST_BADGE, ...value.enamad },
|
||||||
|
samandehi: { ...DEFAULT_WEBSITE_TRUST_BADGE, ...value.samandehi },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function WebsiteBadgesPage() {
|
export function WebsiteBadgesPage() {
|
||||||
const t = useT()
|
const t = useT()
|
||||||
|
const { showToast } = useToast()
|
||||||
|
const [draft, setDraft] = useState<WebsiteTagsAndBadgesSettings>(
|
||||||
|
DEFAULT_WEBSITE_TAGS_AND_BADGES,
|
||||||
|
)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [metaModalOpen, setMetaModalOpen] = useState(false)
|
||||||
|
const [editingMetaId, setEditingMetaId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
void loadSettings(controller.signal)
|
||||||
|
return () => controller.abort()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function loadSettings(signal?: AbortSignal) {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const data = await getSettings(signal)
|
||||||
|
setDraft(normalizeDraft(data.settings.website?.tagsAndBadges))
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
setError(err.message)
|
||||||
|
} else {
|
||||||
|
setError(t('website.badges.errorLoad'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddMetaModal() {
|
||||||
|
setEditingMetaId(null)
|
||||||
|
setMetaModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditMetaModal(id: string) {
|
||||||
|
setEditingMetaId(id)
|
||||||
|
setMetaModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMetaModal() {
|
||||||
|
setMetaModalOpen(false)
|
||||||
|
setEditingMetaId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMetaTag(data: { name: string; content: string }) {
|
||||||
|
setDraft((prev) => {
|
||||||
|
if (editingMetaId) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
metaTags: prev.metaTags.map((tag) =>
|
||||||
|
tag.id === editingMetaId
|
||||||
|
? { ...tag, name: data.name, content: data.content }
|
||||||
|
: tag,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prev.metaTags.length >= MAX_META_TAGS) return prev
|
||||||
|
const next: WebsiteMetaTag = {
|
||||||
|
id: createId(),
|
||||||
|
name: data.name,
|
||||||
|
content: data.content,
|
||||||
|
}
|
||||||
|
return { ...prev, metaTags: [...prev.metaTags, next] }
|
||||||
|
})
|
||||||
|
closeMetaModal()
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMetaTag(id: string) {
|
||||||
|
setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
metaTags: prev.metaTags.filter((tag) => tag.id !== id),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const editingMeta =
|
||||||
|
editingMetaId == null
|
||||||
|
? null
|
||||||
|
: (draft.metaTags.find((tag) => tag.id === editingMetaId) ?? null)
|
||||||
|
|
||||||
|
function updateTrust(kind: TrustKind, patch: Partial<WebsiteTrustBadgeSettings>) {
|
||||||
|
setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[kind]: { ...prev[kind], ...patch },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (isSaving || isLoading) return
|
||||||
|
|
||||||
|
for (const tag of draft.metaTags) {
|
||||||
|
const name = tag.name.trim()
|
||||||
|
const content = tag.content.trim()
|
||||||
|
if (!name && !content) continue
|
||||||
|
if (!name || !META_NAME_RE.test(name)) {
|
||||||
|
setError(t('website.badges.meta.nameInvalid'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!content) {
|
||||||
|
setError(t('website.badges.meta.contentRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const payload: WebsiteTagsAndBadgesSettings = {
|
||||||
|
metaTags: draft.metaTags
|
||||||
|
.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
name: tag.name.trim(),
|
||||||
|
content: tag.content.trim(),
|
||||||
|
}))
|
||||||
|
.filter((tag) => tag.name && tag.content),
|
||||||
|
enamad: {
|
||||||
|
embedHtml: draft.enamad.embedHtml.trim(),
|
||||||
|
},
|
||||||
|
samandehi: {
|
||||||
|
embedHtml: draft.samandehi.embedHtml.trim(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await updateWebsiteSettings({ tagsAndBadges: payload })
|
||||||
|
setDraft(normalizeDraft(data.settings.website?.tagsAndBadges ?? payload))
|
||||||
|
showToast(t('website.badges.saved'), 'success')
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
setError(err.message)
|
||||||
|
} else {
|
||||||
|
setError(t('website.badges.errorSave'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebsiteSectionShell
|
<main className={pageStyles.content}>
|
||||||
sectionLabel="Badges"
|
<Breadcrumbs
|
||||||
title={t('title.badges')}
|
items={[
|
||||||
subtitle={t('website.badges.subtitle')}
|
{ label: 'Dashboard', href: '/' },
|
||||||
|
{ label: 'Website', href: '/website' },
|
||||||
|
{ label: 'Badges' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={pageStyles.pageHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={pageStyles.pageTitle}>{t('title.badges')}</h2>
|
||||||
|
<p className={pageStyles.pageSubtitle}>{t('website.badges.subtitle')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div className={sharedStyles.alertError} role="alert">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<section className={`${sharedStyles.panel} ${settingsStyles.schemaPanel}`}>
|
||||||
|
<div className={settingsStyles.schemaHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={settingsStyles.schemaTitle}>{t('website.badges.meta.title')}</h2>
|
||||||
|
<p className={`${settingsStyles.schemaDesc} ${styles.metaDesc}`}>
|
||||||
|
<span>{t('website.badges.meta.desc')}</span>{' '}
|
||||||
|
<code className={styles.metaSample} dir="ltr">
|
||||||
|
{t('website.badges.meta.sample')}
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Tooltip label={t('website.badges.meta.add')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={settingsStyles.addSameAsBtn}
|
||||||
|
onClick={openAddMetaModal}
|
||||||
|
disabled={isLoading || isSaving || draft.metaTags.length >= MAX_META_TAGS}
|
||||||
|
aria-label={t('website.badges.meta.add')}
|
||||||
>
|
>
|
||||||
<p className={styles.placeholderLead}>{t('website.badges.lead')}</p>
|
<Plus size={16} />
|
||||||
<p className={styles.placeholderNote}>{t('website.badges.note')}</p>
|
</button>
|
||||||
<ul className={styles.featureList}>
|
</Tooltip>
|
||||||
<li>{t('website.badges.feature.upload')}</li>
|
</div>
|
||||||
<li>{t('website.badges.feature.reorder')}</li>
|
|
||||||
<li>{t('website.badges.feature.toggle')}</li>
|
{draft.metaTags.length === 0 ? (
|
||||||
|
<p className={styles.emptyHint}>{t('website.badges.meta.empty')}</p>
|
||||||
|
) : (
|
||||||
|
<ul className={styles.metaList} dir="ltr">
|
||||||
|
{draft.metaTags.map((tag) => (
|
||||||
|
<li key={tag.id} className={styles.metaRow}>
|
||||||
|
<code className={styles.metaName}>{tag.name}</code>
|
||||||
|
<span className={styles.metaValue} title={tag.content}>
|
||||||
|
{tag.content}
|
||||||
|
</span>
|
||||||
|
<div className={styles.metaActions}>
|
||||||
|
<Tooltip label={t('website.badges.meta.edit')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={controlStyles.controlBtn}
|
||||||
|
onClick={() => openEditMetaModal(tag.id)}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
aria-label={t('website.badges.meta.edit')}
|
||||||
|
>
|
||||||
|
<Pencil size={15} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('website.badges.meta.remove')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${controlStyles.controlBtn} ${controlStyles.danger}`}
|
||||||
|
onClick={() => removeMetaTag(tag.id)}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
aria-label={t('website.badges.meta.remove')}
|
||||||
|
>
|
||||||
|
<Trash2 size={15} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</WebsiteSectionShell>
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<WebsiteMetaTagModal
|
||||||
|
open={metaModalOpen}
|
||||||
|
onClose={closeMetaModal}
|
||||||
|
onSubmit={saveMetaTag}
|
||||||
|
initial={
|
||||||
|
editingMeta
|
||||||
|
? { name: editingMeta.name, content: editingMeta.content }
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TrustBadgePanel
|
||||||
|
kind="enamad"
|
||||||
|
title={t('website.badges.enamad.title')}
|
||||||
|
desc={t('website.badges.enamad.desc')}
|
||||||
|
value={draft.enamad}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
onChange={(patch) => updateTrust('enamad', patch)}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TrustBadgePanel
|
||||||
|
kind="samandehi"
|
||||||
|
title={t('website.badges.samandehi.title')}
|
||||||
|
desc={t('website.badges.samandehi.desc')}
|
||||||
|
value={draft.samandehi}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
onChange={(patch) => updateTrust('samandehi', patch)}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={settingsStyles.saveSchemaBtn}
|
||||||
|
onClick={() => void handleSave()}
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? t('website.badges.saving') : t('website.badges.save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrustBadgePanel({
|
||||||
|
kind,
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
kind: TrustKind
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
value: WebsiteTrustBadgeSettings
|
||||||
|
disabled: boolean
|
||||||
|
onChange: (patch: Partial<WebsiteTrustBadgeSettings>) => void
|
||||||
|
t: (key: BusinessMessageKey) => string
|
||||||
|
}) {
|
||||||
|
const embedId = `trust-${kind}-embed`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`${sharedStyles.panel} ${settingsStyles.schemaPanel}`}>
|
||||||
|
<div className={settingsStyles.schemaHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={settingsStyles.schemaTitle}>{title}</h2>
|
||||||
|
<p className={`${settingsStyles.schemaDesc} ${styles.oneLineHint}`}>{desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={settingsStyles.schemaGrid}>
|
||||||
|
<div className={`${settingsStyles.field} ${settingsStyles.schemaField} ${settingsStyles.schemaFieldFull}`}>
|
||||||
|
<textarea
|
||||||
|
id={embedId}
|
||||||
|
rows={4}
|
||||||
|
value={value.embedHtml}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={t('website.badges.trust.embedPlaceholder')}
|
||||||
|
aria-label={title}
|
||||||
|
dir="ltr"
|
||||||
|
onChange={(e) => onChange({ embedHtml: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,32 @@ export interface WebsiteSchemaSettings {
|
|||||||
sameAs: string[]
|
sameAs: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebsiteMetaTag {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebsiteTrustBadgeSettings {
|
||||||
|
embedHtml: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebsiteTagsAndBadgesSettings {
|
||||||
|
metaTags: WebsiteMetaTag[]
|
||||||
|
enamad: WebsiteTrustBadgeSettings
|
||||||
|
samandehi: WebsiteTrustBadgeSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_WEBSITE_TRUST_BADGE: WebsiteTrustBadgeSettings = {
|
||||||
|
embedHtml: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_WEBSITE_TAGS_AND_BADGES: WebsiteTagsAndBadgesSettings = {
|
||||||
|
metaTags: [],
|
||||||
|
enamad: { ...DEFAULT_WEBSITE_TRUST_BADGE },
|
||||||
|
samandehi: { ...DEFAULT_WEBSITE_TRUST_BADGE },
|
||||||
|
}
|
||||||
|
|
||||||
export interface WebsiteSitemapConfig {
|
export interface WebsiteSitemapConfig {
|
||||||
baseUrl?: string | null
|
baseUrl?: string | null
|
||||||
staticPages?: Array<{
|
staticPages?: Array<{
|
||||||
@@ -126,6 +152,7 @@ export interface WebsiteSettings {
|
|||||||
specialProductsSource: SpecialProductsSource
|
specialProductsSource: SpecialProductsSource
|
||||||
sitemapConfig?: WebsiteSitemapConfig | null
|
sitemapConfig?: WebsiteSitemapConfig | null
|
||||||
schema?: WebsiteSchemaSettings
|
schema?: WebsiteSchemaSettings
|
||||||
|
tagsAndBadges?: WebsiteTagsAndBadgesSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SmsSettings {
|
export interface SmsSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user