From 672091d1f51245f4bdbc2ccf82270a3cdc2e8c59 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Mon, 3 Aug 2026 00:06:44 +0330 Subject: [PATCH] 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 --- .../src/components/AddCustomerModal.tsx | 86 +- .../components/AddWebsiteSliderSlideModal.tsx | 29 +- apps/business/src/components/BlogCard.tsx | 52 +- .../src/components/BlogCategoryRow.tsx | 34 +- .../src/components/BlogCommentsModal.tsx | 5 +- .../src/components/BlogCommentsSection.tsx | 7 +- apps/business/src/components/BrandModal.tsx | 45 +- apps/business/src/components/BrandRow.tsx | 24 +- .../src/components/CategoryModal.module.css | 7 + .../business/src/components/CategoryModal.tsx | 40 +- .../src/components/CategoryRow.module.css | 8 +- apps/business/src/components/CategoryRow.tsx | 42 +- .../ContactSubmissionDetailModal.tsx | 29 +- .../src/components/CreateStoreItemsModal.tsx | 46 +- .../src/components/EditStoreItemsModal.tsx | 44 +- apps/business/src/components/ImageCropper.tsx | 32 +- .../business/src/components/ImageUploader.tsx | 10 +- .../components/ListFiltersPanel.module.css | 27 +- .../src/components/OrderTransactionsModal.tsx | 13 +- .../components/PickStoreSpecialItemsModal.tsx | 36 +- .../src/components/PickStoreVariantModal.tsx | 22 +- .../src/components/PickWebsiteBrandsModal.tsx | 38 +- .../components/PickWebsiteCategoriesModal.tsx | 38 +- .../src/components/PortfolioCard.module.css | 4 + .../business/src/components/PortfolioCard.tsx | 46 +- .../src/components/PortfolioCommentsModal.tsx | 5 +- .../components/PortfolioCommentsSection.tsx | 7 +- .../src/components/ProductCard.module.css | 51 +- apps/business/src/components/ProductCard.tsx | 71 +- .../src/components/ProductCommentsModal.tsx | 5 +- .../src/components/ProductDetailsTabs.tsx | 11 +- .../src/components/ProductSearchSelect.tsx | 71 +- .../components/SearchableSelect.module.css | 19 +- .../src/components/SearchableSelect.tsx | 59 +- apps/business/src/components/Sidebar.tsx | 15 +- .../src/components/StoreItemCard.module.css | 54 +- .../business/src/components/StoreItemCard.tsx | 78 +- .../StoreItemMultiSearchSelect.module.css | 16 +- .../components/StoreItemMultiSearchSelect.tsx | 61 +- .../StoreSpecialCarousel.module.css | 6 + .../src/components/StoreSpecialCarousel.tsx | 129 +- .../src/components/StoreSpecialModal.tsx | 91 +- .../src/components/Tooltip.module.css | 1 + .../WebsiteGroupCarousel.module.css | 6 + .../src/components/WebsiteGroupCarousel.tsx | 117 +- .../WebsiteGroupItemCard.module.css | 37 +- .../src/components/WebsiteGroupItemCard.tsx | 48 +- .../WebsiteSliderGallery.module.css | 4 +- .../src/components/WebsiteSliderGallery.tsx | 62 +- .../src/components/WebsiteSliderSlideCard.tsx | 16 +- apps/business/src/data/productComments.ts | 4 +- apps/business/src/i18n/messages.ts | 1471 +++++++++++++++++ apps/business/src/pages/AddNewBlogPage.tsx | 60 +- .../src/pages/AddNewPortfolioPage.tsx | 73 +- apps/business/src/pages/AddNewProductPage.tsx | 51 +- .../business/src/pages/BlogCategoriesPage.tsx | 34 +- apps/business/src/pages/BlogDetailsPage.tsx | 33 +- apps/business/src/pages/BlogListPage.tsx | 29 +- apps/business/src/pages/BlogPage.module.css | 8 +- apps/business/src/pages/BlogPage.tsx | 51 +- apps/business/src/pages/BlogSettingsPage.tsx | 27 +- apps/business/src/pages/BrandsPage.module.css | 8 +- apps/business/src/pages/BrandsPage.tsx | 38 +- .../src/pages/CategoriesPage.module.css | 8 +- apps/business/src/pages/CategoriesPage.tsx | 40 +- .../src/pages/CustomersPage.module.css | 48 +- apps/business/src/pages/CustomersPage.tsx | 229 +-- .../src/pages/MyProductsPage.module.css | 21 +- apps/business/src/pages/MyProductsPage.tsx | 54 +- apps/business/src/pages/OrdersPage.module.css | 6 +- apps/business/src/pages/OrdersPage.tsx | 198 ++- .../src/pages/PortfolioCategoriesPage.tsx | 34 +- .../src/pages/PortfolioDetailsPage.tsx | 25 +- apps/business/src/pages/PortfolioListPage.tsx | 27 +- .../src/pages/PortfolioSettingsPage.tsx | 27 +- apps/business/src/pages/PortfoliosPage.tsx | 51 +- .../src/pages/ProductSettingsPage.tsx | 40 +- apps/business/src/pages/ShoppingCardsPage.tsx | 105 +- .../src/pages/StoreItemsPage.module.css | 8 +- apps/business/src/pages/StoreItemsPage.tsx | 68 +- apps/business/src/pages/StorePage.tsx | 50 +- apps/business/src/pages/StoreSettingsPage.tsx | 74 +- apps/business/src/pages/StoreSpecialsPage.tsx | 77 +- apps/business/src/pages/WebsiteBadgesPage.tsx | 21 +- .../business/src/pages/WebsiteContactPage.tsx | 74 +- .../src/pages/WebsiteEPaymentPage.tsx | 21 +- apps/business/src/pages/WebsiteFaqPage.tsx | 21 +- apps/business/src/pages/WebsitePage.tsx | 81 +- .../business/src/pages/WebsiteSlidersPage.tsx | 30 +- .../src/pages/WebsiteSpecialBrandsPage.tsx | 75 +- .../pages/WebsiteSpecialCategoriesPage.tsx | 75 +- .../src/pages/WebsiteSubscriptionsPage.tsx | 21 +- apps/business/src/services/blogService.ts | 16 +- .../src/services/expertReviewService.ts | 4 +- .../business/src/services/portfolioService.ts | 16 +- apps/business/src/services/productService.ts | 2 + .../src/services/storeSpecialService.ts | 2 + apps/business/src/types/product.ts | 1 + apps/business/src/types/storeSpecial.ts | 1 + apps/business/src/types/websiteBrandGroup.ts | 3 + .../src/types/websiteCategoryGroup.ts | 3 + apps/business/src/utils/storeProductGroups.ts | 5 +- apps/super-admin/src/pages/WebsitesPage.tsx | 40 + .../super-admin/src/services/domainService.ts | 9 +- apps/super-admin/src/types/domain.ts | 5 + deploy/meshkee-ssl-sync-agent.service | 14 + deploy/ssl-sync-agent.mjs | 93 ++ deploy/ssl-sync.sh | 2 +- docs/DEPLOY.md | 44 +- docs/PROJECT_CONTEXT.md | 2 +- .../src/components/Pagination.module.css | 18 +- .../src/components/Pagination.tsx | 23 +- .../src/context/LocaleContext.tsx | 2 + 113 files changed, 4154 insertions(+), 1451 deletions(-) create mode 100644 deploy/meshkee-ssl-sync-agent.service create mode 100644 deploy/ssl-sync-agent.mjs diff --git a/apps/business/src/components/AddCustomerModal.tsx b/apps/business/src/components/AddCustomerModal.tsx index a32b409..1c290a0 100644 --- a/apps/business/src/components/AddCustomerModal.tsx +++ b/apps/business/src/components/AddCustomerModal.tsx @@ -1,6 +1,8 @@ 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 { ApiError } from '../lib/api' import { toE164CellNumber } from '../lib/cellNumber' import { @@ -19,13 +21,16 @@ interface AddCustomerModalProps { const ANIMATION_MS = 220 export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [firstName, setFirstName] = useState('') const [lastName, setLastName] = useState('') const [cellNumber, setCellNumber] = useState('') const [password, setPassword] = useState('') - const [email, setEmail] = useState('') + const [passwordConfirm, setPasswordConfirm] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState('') @@ -37,7 +42,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP setLastName('') setCellNumber('') setPassword('') - setEmail('') + setPasswordConfirm('') setError('') } else if (mounted) { setClosing(true) @@ -69,6 +74,18 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP const normalizedCell = toE164CellNumber(cellNumber.trim()) if (!canSubmit || !normalizedCell) return + const trimmedPassword = password.trim() + if (trimmedPassword || passwordConfirm.trim()) { + if (trimmedPassword.length < 8) { + setError(t('customers.addModal.errorPasswordLength')) + return + } + if (trimmedPassword !== passwordConfirm.trim()) { + setError(t('customers.addModal.errorPasswordMatch')) + return + } + } + setIsSubmitting(true) setError('') try { @@ -76,8 +93,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP cellNumber: normalizedCell, firstName: firstName.trim(), lastName: lastName.trim(), - ...(password.trim() ? { password: password.trim() } : {}), - ...(email.trim() ? { email: email.trim() } : {}), + ...(trimmedPassword ? { password: trimmedPassword } : {}), }) onCreated({ id: created.id, @@ -91,7 +107,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP }) onClose() } catch (err) { - setError(err instanceof ApiError ? err.message : 'Unable to add customer.') + setError(err instanceof ApiError ? err.message : t('customers.addModal.error')) } finally { setIsSubmitting(false) } @@ -108,79 +124,91 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP role="dialog" aria-modal="true" aria-labelledby="add-customer-title" + lang={isFa ? 'fa' : 'en'} + dir={isFa ? 'rtl' : 'ltr'} >

- Add customer + {t('customers.addModal.title')}

-

- Creates a verified customer account or links an existing user to your business. -

+

{t('customers.addModal.subtitle')}

-
-

- Password is required only for new accounts. Existing users are added as verified - customers. -

+

{t('customers.addModal.hint')}

{error &&

{error}

}
- + setFirstName(e.target.value)} disabled={isSubmitting} + dir={isFa ? 'rtl' : 'ltr'} />
- + setLastName(e.target.value)} disabled={isSubmitting} + dir={isFa ? 'rtl' : 'ltr'} />
- + setCellNumber(e.target.value)} - placeholder="0912..." + placeholder={t('customers.addModal.cellPlaceholder')} autoComplete="off" disabled={isSubmitting} + inputMode="tel" + dir="ltr" />
-
- +
+ setPassword(e.target.value)} - placeholder="Min. 8 characters" + placeholder={t('customers.addModal.passwordPlaceholder')} disabled={isSubmitting} + dir="ltr" />
-
- +
+ setEmail(e.target.value)} + id="add-customer-password-confirm" + type="password" + autoComplete="new-password" + value={passwordConfirm} + onChange={(e) => setPasswordConfirm(e.target.value)} + placeholder={t('customers.addModal.passwordConfirmPlaceholder')} disabled={isSubmitting} + dir="ltr" />
@@ -192,7 +220,7 @@ export function AddCustomerModal({ open, onClose, onCreated }: AddCustomerModalP onClick={onClose} disabled={isSubmitting} > - Cancel + {t('customers.addModal.cancel')}
diff --git a/apps/business/src/components/AddWebsiteSliderSlideModal.tsx b/apps/business/src/components/AddWebsiteSliderSlideModal.tsx index fbfc751..69386a0 100644 --- a/apps/business/src/components/AddWebsiteSliderSlideModal.tsx +++ b/apps/business/src/components/AddWebsiteSliderSlideModal.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { ImageCropper } from './ImageCropper' +import { useT } from '../i18n/useT' import modalStyles from './CategoryModal.module.css' import styles from './AddWebsiteSliderSlideModal.module.css' @@ -26,6 +27,7 @@ export function AddWebsiteSliderSlideModal({ onSubmit, isSubmitting = false, }: AddWebsiteSliderSlideModalProps) { + const t = useT() const formRef = useRef(null) const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) @@ -85,32 +87,37 @@ export function AddWebsiteSliderSlideModal({ >

- Add slide + {t('website.sliders.modal.title')}

-
- +
- + setTitle(e.target.value)} disabled={isSubmitting} @@ -118,7 +125,7 @@ export function AddWebsiteSliderSlideModal({
- + - Cancel + {t('products.form.cancel')}
diff --git a/apps/business/src/components/BlogCard.tsx b/apps/business/src/components/BlogCard.tsx index b5db0d8..72c82b3 100644 --- a/apps/business/src/components/BlogCard.tsx +++ b/apps/business/src/components/BlogCard.tsx @@ -1,11 +1,13 @@ import { Link } from 'react-router-dom' import { Pencil, MessageSquare, Trash2, BadgeCheck, Clock } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' import type { Blog } from '../types/blog' import { formatBlogAuthor, formatBlogCardDate, isBlogVerified, } from '../services/blogService' +import { useT } from '../i18n/useT' import { textLocaleAttrs } from '../utils/textLocale' import { Tooltip } from './Tooltip' import cardStyles from './BlogCard.module.css' @@ -30,11 +32,21 @@ export function BlogCard({ onRemove, isVerifying = false, }: BlogCardProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' const verified = isBlogVerified(blog) - const publishDate = formatBlogCardDate(blog.publishedAt ?? blog.createdAt) + const publishDate = formatBlogCardDate( + blog.publishedAt ?? blog.createdAt, + isFa ? 'fa' : 'en', + ) const titleLocale = textLocaleAttrs(blog.title) const abstractLocale = textLocaleAttrs(blog.abstract) const hasAbstract = Boolean(blog.abstract?.trim()) + const verifyLabel = verified ? t('blog.card.unverify') : t('blog.card.verify') + // In FA UI, keep cards RTL even when title/abstract are English. + const titleDir = isFa ? 'rtl' : titleLocale.dir + const abstractDir = isFa ? 'rtl' : abstractLocale.dir return (
@@ -53,21 +65,21 @@ export function BlogCard({ {!verified && ( )} {verified && ( - Verified + {t('blog.card.verified')} )}
-
+

{blog.title}

@@ -77,13 +89,17 @@ export function BlogCard({ .filter(Boolean) .join(' ')} lang={abstractLocale.lang} - dir={abstractLocale.dir} + dir={abstractDir} > {blog.abstract}

) : ( -

- No summary yet. +

+ {t('blog.card.noSummary')}

)}

@@ -95,28 +111,32 @@ export function BlogCard({

- - - + - + - + diff --git a/apps/business/src/components/BlogCategoryRow.tsx b/apps/business/src/components/BlogCategoryRow.tsx index 3c76c85..012a2c7 100644 --- a/apps/business/src/components/BlogCategoryRow.tsx +++ b/apps/business/src/components/BlogCategoryRow.tsx @@ -1,5 +1,7 @@ import { FolderPlus, Pencil, Trash2, ChevronRight } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' import type { Category } from '../types/category' +import { useT } from '../i18n/useT' import { Tooltip } from './Tooltip' import styles from './CategoryRow.module.css' @@ -24,6 +26,16 @@ export function BlogCategoryRow({ onEdit, onRemove, }: BlogCategoryRowProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' + const primaryName = isFa ? category.nameFa || category.nameEn : category.nameEn + const secondaryName = isFa + ? category.nameFa + ? category.nameEn + : '' + : category.nameFa + return (
- {category.nameEn} - · - {category.nameFa} + {primaryName} + {secondaryName ? ( + <> + · + {secondaryName} + + ) : null} {hasChildren && ( @@ -59,32 +75,32 @@ export function BlogCategoryRow({
- + - + - + diff --git a/apps/business/src/components/BlogCommentsModal.tsx b/apps/business/src/components/BlogCommentsModal.tsx index 9dcff49..b12db88 100644 --- a/apps/business/src/components/BlogCommentsModal.tsx +++ b/apps/business/src/components/BlogCommentsModal.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { X, ThumbsUp, Check, XCircle, Trash2 } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' import type { ProductComment } from '../types/comment' import { formatCommentDate } from '../data/productComments' import { ApiError } from '../lib/api' @@ -30,6 +31,8 @@ export function BlogCommentsModal({ onClose, onCountChange, }: BlogCommentsModalProps) { + const { locale } = useLocale() + const dateLocale = locale === 'fa' ? 'fa' : 'en' const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [comments, setComments] = useState([]) @@ -200,7 +203,7 @@ export function BlogCommentsModal({
{comment.author}
- {formatCommentDate(comment.createdAt)} + {formatCommentDate(comment.createdAt, dateLocale)}
diff --git a/apps/business/src/components/BlogCommentsSection.tsx b/apps/business/src/components/BlogCommentsSection.tsx index f52a9a6..df593a0 100644 --- a/apps/business/src/components/BlogCommentsSection.tsx +++ b/apps/business/src/components/BlogCommentsSection.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { ThumbsUp, Check, XCircle, Trash2 } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' import type { ProductComment } from '../types/comment' import { formatCommentDate } from '../data/productComments' import { ApiError } from '../lib/api' @@ -18,6 +19,8 @@ interface BlogCommentsSectionProps { } export function BlogCommentsSection({ blogId, onCountChange }: BlogCommentsSectionProps) { + const { locale } = useLocale() + const dateLocale = locale === 'fa' ? 'fa' : 'en' const [comments, setComments] = useState([]) const [totalComments, setTotalComments] = useState(0) const [currentPage, setCurrentPage] = useState(1) @@ -130,7 +133,9 @@ export function BlogCommentsSection({ blogId, onCountChange }: BlogCommentsSecti
{comment.author}
-
{formatCommentDate(comment.createdAt)}
+
+ {formatCommentDate(comment.createdAt, dateLocale)} +
diff --git a/apps/business/src/components/BrandModal.tsx b/apps/business/src/components/BrandModal.tsx index 7897fa6..ec0c88e 100644 --- a/apps/business/src/components/BrandModal.tsx +++ b/apps/business/src/components/BrandModal.tsx @@ -1,6 +1,8 @@ import { useEffect, useRef, 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 { ImageCropper } from './ImageCropper' import type { Brand, BrandFormData } from '../types/brand' import styles from './BrandModal.module.css' @@ -21,9 +23,12 @@ export function BrandModal({ onClose, onSubmit, editingBrand = null, - title = 'Add Brand', + title, isSubmitting = false, }: BrandModalProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' const formRef = useRef(null) const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) @@ -85,39 +90,46 @@ export function BrandModal({ role="dialog" aria-modal="true" aria-labelledby="brand-modal-title" + lang={isFa ? 'fa' : 'en'} + dir={isFa ? 'rtl' : 'ltr'} >

- {title} + {title ?? t('brands.add')}

-
- +
- + setNameFa(e.target.value)} disabled={isSubmitting} @@ -125,13 +137,13 @@ export function BrandModal({
- + setNameEn(e.target.value)} required @@ -140,12 +152,13 @@ export function BrandModal({
- +