diff --git a/apps/business/src/App.tsx b/apps/business/src/App.tsx
index 03cbc89..26ddf69 100644
--- a/apps/business/src/App.tsx
+++ b/apps/business/src/App.tsx
@@ -1,4 +1,4 @@
-import { BrowserRouter, Routes, Route } from 'react-router-dom'
+import { BrowserRouter, Navigate, Routes, Route } from 'react-router-dom'
import { BusinessDomainGuard } from './components/BusinessDomainGuard'
import { DashboardDocumentTitle } from './components/DashboardDocumentTitle'
import { AuthProvider } from './context/AuthContext'
@@ -66,7 +66,7 @@ import { WebsiteContactPage } from './pages/WebsiteContactPage'
import { WebsiteSubscriptionsPage } from './pages/WebsiteSubscriptionsPage'
import { WebsiteFaqPage } from './pages/WebsiteFaqPage'
import { WebsiteBadgesPage } from './pages/WebsiteBadgesPage'
-import { WebsiteEPaymentPage } from './pages/WebsiteEPaymentPage'
+import { SettingsPage } from './pages/SettingsPage'
import { WebsiteSlidersPage } from './pages/WebsiteSlidersPage'
import { WebsiteSpecialCategoriesPage } from './pages/WebsiteSpecialCategoriesPage'
import { WebsiteSpecialBrandsPage } from './pages/WebsiteSpecialBrandsPage'
@@ -91,6 +91,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
@@ -155,7 +156,7 @@ function App() {
} />
} />
} />
- } />
+ } />
diff --git a/apps/business/src/components/CategoryModal.module.css b/apps/business/src/components/CategoryModal.module.css
index 65f57e0..42ffe7b 100644
--- a/apps/business/src/components/CategoryModal.module.css
+++ b/apps/business/src/components/CategoryModal.module.css
@@ -152,6 +152,19 @@
box-shadow: 0 6px 16px rgba(var(--primary-rgb) / 0.35);
}
+.submitBtn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+}
+
+.errorText {
+ margin: 0;
+ font-size: 12px;
+ color: #ef4444;
+}
+
.modalIn {
animation: modalFadeIn 0.25s ease forwards;
}
diff --git a/apps/business/src/components/CategorySearchSelect.module.css b/apps/business/src/components/CategorySearchSelect.module.css
index 4d39603..020646a 100644
--- a/apps/business/src/components/CategorySearchSelect.module.css
+++ b/apps/business/src/components/CategorySearchSelect.module.css
@@ -81,18 +81,16 @@
}
.dropdown {
- position: absolute;
- top: calc(100% + 6px);
- inset-inline: 0;
- max-height: 260px;
- overflow-y: auto;
+ position: static;
+ margin-top: 6px;
+ max-height: none;
+ overflow: visible;
background: var(--elevated-surface);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
box-shadow: var(--glass-shadow);
- z-index: 30;
list-style: none;
padding: 6px;
margin: 0;
diff --git a/apps/business/src/components/CitySearchSelect.module.css b/apps/business/src/components/CitySearchSelect.module.css
index 72ade7c..bf18b57 100644
--- a/apps/business/src/components/CitySearchSelect.module.css
+++ b/apps/business/src/components/CitySearchSelect.module.css
@@ -81,18 +81,16 @@
}
.dropdown {
- position: absolute;
- top: calc(100% + 6px);
- inset-inline: 0;
- max-height: 220px;
- overflow-y: auto;
+ position: static;
+ margin-top: 6px;
+ max-height: none;
+ overflow: visible;
background: var(--elevated-surface);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
box-shadow: var(--glass-shadow);
- z-index: 20;
list-style: none;
padding: 6px;
margin: 0;
diff --git a/apps/business/src/components/MultiSelectDropdown.module.css b/apps/business/src/components/MultiSelectDropdown.module.css
index 1fdfadf..674ad2e 100644
--- a/apps/business/src/components/MultiSelectDropdown.module.css
+++ b/apps/business/src/components/MultiSelectDropdown.module.css
@@ -64,15 +64,13 @@
}
.dropdown {
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- z-index: 20;
+ position: static;
+ margin-top: 4px;
+ z-index: auto;
display: flex;
flex-direction: column;
- max-height: 240px;
- overflow: hidden;
+ max-height: none;
+ overflow: visible;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
diff --git a/apps/business/src/components/PageContent.module.css b/apps/business/src/components/PageContent.module.css
index 8b1f580..d1f395a 100644
--- a/apps/business/src/components/PageContent.module.css
+++ b/apps/business/src/components/PageContent.module.css
@@ -108,6 +108,10 @@
margin-top: 24px;
}
+.span4 {
+ grid-column: span 4;
+}
+
.col6 {
grid-column: span 6;
}
@@ -121,6 +125,10 @@
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
+
+ .span4 {
+ grid-column: span 2;
+ }
}
@media (max-width: 1400px) {
@@ -133,6 +141,10 @@
.gridHome {
grid-template-columns: repeat(3, 1fr);
}
+
+ .span4 {
+ grid-column: span 2;
+ }
}
@media (max-width: 900px) {
@@ -141,6 +153,10 @@
.gridCols4 {
grid-template-columns: repeat(2, 1fr);
}
+
+ .span4 {
+ grid-column: span 2;
+ }
}
@media (max-width: 768px) {
@@ -173,7 +189,8 @@
gap: 16px;
}
+ .span4,
.col6 {
- grid-column: span 12;
+ grid-column: span 1;
}
}
diff --git a/apps/business/src/components/SearchableSelect.module.css b/apps/business/src/components/SearchableSelect.module.css
index 68f0379..8ba1cab 100644
--- a/apps/business/src/components/SearchableSelect.module.css
+++ b/apps/business/src/components/SearchableSelect.module.css
@@ -69,31 +69,29 @@
}
.dropdown {
- position: absolute;
- top: calc(100% + 6px);
- left: 0;
- right: 0;
- max-height: 220px;
- overflow-y: auto;
+ position: static;
+ margin-top: 6px;
+ max-height: none;
+ overflow: visible;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
box-shadow: 0 8px 24px rgba(31, 38, 135, 0.12);
- z-index: 10;
list-style: none;
padding: 6px;
animation: dropdownIn 0.15s ease;
}
-/* Prefer for selects inside overflow-clipped modals — expands parent instead of clipping. */
+/* Alias — in-flow is the default so search results expand the parent modal. */
.dropdownInFlow {
position: static;
top: auto;
left: auto;
right: auto;
- max-height: min(50vh, 400px);
+ max-height: none;
+ overflow: visible;
margin-top: 6px;
z-index: auto;
}
diff --git a/apps/business/src/components/SectionCard.tsx b/apps/business/src/components/SectionCard.tsx
index bde173c..a385bd0 100644
--- a/apps/business/src/components/SectionCard.tsx
+++ b/apps/business/src/components/SectionCard.tsx
@@ -8,6 +8,7 @@ interface SectionCardProps {
description: string
linkText?: string
href: string
+ className?: string
/** Entity total shown beside the arrow; omit for sections without a count (e.g. settings). */
count?: number | null
countLabel?: string
@@ -18,6 +19,7 @@ export function SectionCard({
title,
description,
href,
+ className,
count,
countLabel,
}: SectionCardProps) {
@@ -27,7 +29,7 @@ export function SectionCard({
: null
return (
-
+
diff --git a/apps/business/src/components/Sidebar.tsx b/apps/business/src/components/Sidebar.tsx
index 3df9385..6fc6604 100644
--- a/apps/business/src/components/Sidebar.tsx
+++ b/apps/business/src/components/Sidebar.tsx
@@ -212,7 +212,6 @@ export function Sidebar() {
{ labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' },
{ labelKey: 'nav.website.faq', to: '/website/faq' },
{ labelKey: 'nav.website.badges', to: '/website/badges' },
- { labelKey: 'nav.website.ePayment', to: '/website/e-payment' },
],
},
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
diff --git a/apps/business/src/components/StoreItemDiscountModal.tsx b/apps/business/src/components/StoreItemDiscountModal.tsx
index 7a3e1d7..3e3b894 100644
--- a/apps/business/src/components/StoreItemDiscountModal.tsx
+++ b/apps/business/src/components/StoreItemDiscountModal.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 {
updateStoreItemDiscounts,
@@ -15,6 +17,7 @@ import rowStyles from './CreateStoreItemsModal.module.css'
interface StoreItemDiscountModalProps {
open: boolean
productTitle: string
+ productNameFa?: string
items: StoreItem[]
onClose: () => void
onSaved?: (items: StoreItem[]) => void
@@ -33,10 +36,15 @@ const ANIMATION_MS = 220
export function StoreItemDiscountModal({
open,
productTitle,
+ productNameFa = '',
items,
onClose,
onSaved,
}: StoreItemDiscountModalProps) {
+ const t = useT()
+ const { locale } = useLocale()
+ const isFa = locale === 'fa'
+ const displayTitle = isFa ? productNameFa || productTitle : productTitle || productNameFa
const [mounted, setMounted] = useState(open)
const [closing, setClosing] = useState(false)
const [rows, setRows] = useState([])
@@ -113,7 +121,7 @@ export function StoreItemDiscountModal({
if (err instanceof ApiError) {
setError(err.message)
} else {
- setError('Unable to save discounts.')
+ setError(t('storeItems.discount.errorSave'))
}
} finally {
setIsSubmitting(false)
@@ -131,24 +139,26 @@ export function StoreItemDiscountModal({
role="dialog"
aria-modal="true"
aria-labelledby="store-discount-title"
+ lang={isFa ? 'fa' : 'en'}
+ dir={isFa ? 'rtl' : 'ltr'}
>
- Set Discounts
+ {t('storeItems.discount.title')}
-
{productTitle}
+
{displayTitle}
-
diff --git a/apps/business/src/components/StoreItemMultiSearchSelect.module.css b/apps/business/src/components/StoreItemMultiSearchSelect.module.css
index 87772ab..04b79b4 100644
--- a/apps/business/src/components/StoreItemMultiSearchSelect.module.css
+++ b/apps/business/src/components/StoreItemMultiSearchSelect.module.css
@@ -4,10 +4,6 @@
gap: 12px;
}
-.dropdown {
- max-height: 280px;
-}
-
.option {
display: flex;
align-items: center;
diff --git a/apps/business/src/components/StoreItemMultiSearchSelect.tsx b/apps/business/src/components/StoreItemMultiSearchSelect.tsx
index c62e22f..1b8a468 100644
--- a/apps/business/src/components/StoreItemMultiSearchSelect.tsx
+++ b/apps/business/src/components/StoreItemMultiSearchSelect.tsx
@@ -125,7 +125,7 @@ export function StoreItemMultiSearchSelect({
{showDropdown && (
-
+
{filtered.length === 0 ? (
-
{options.length === 0
@@ -164,7 +164,7 @@ export function StoreItemMultiSearchSelect({
)}
{open && query.trim().length > 0 && query.trim().length < minSearchLength && (
-
+
-
{t('storeItems.search.minChars', { count: minSearchLength })}
diff --git a/apps/business/src/components/StoreSpecialModal.tsx b/apps/business/src/components/StoreSpecialModal.tsx
index d58c5c4..d526493 100644
--- a/apps/business/src/components/StoreSpecialModal.tsx
+++ b/apps/business/src/components/StoreSpecialModal.tsx
@@ -19,11 +19,45 @@ interface StoreSpecialModalProps {
title?: string
submitLabel?: string
isSubmitting?: boolean
+ error?: string
}
const ANIMATION_MS = 220
const KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/
+function slugifyGroupKey(raw: string): string {
+ const slug = raw
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ if (!slug || !/^[a-z]/.test(slug)) return ''
+ return KEY_PATTERN.test(slug) ? slug : ''
+}
+
+function hasNonAscii(value: string): boolean {
+ return /[^\u0000-\u007F]/.test(value)
+}
+
+function resolveGroupFields(keyRaw: string, titleRaw: string): StoreSpecialModalValues | null {
+ const key = keyRaw.trim()
+ const title = titleRaw.trim()
+ const keySlug = slugifyGroupKey(key)
+ const titleSlug = slugifyGroupKey(title)
+ const keyLooksLikeTitle = key.length > 0 && !keySlug && hasNonAscii(key)
+
+ if (keySlug && title) return { key: keySlug, title }
+ if (keyLooksLikeTitle && titleSlug) return { key: titleSlug, title: key }
+ if (!key && titleSlug) return { key: titleSlug, title }
+ if (title && !keySlug && !titleSlug) {
+ return { key: `special-${Date.now().toString(36)}`, title }
+ }
+ if (keyLooksLikeTitle && !title) {
+ return { key: `special-${Date.now().toString(36)}`, title: key }
+ }
+ return null
+}
+
export function StoreSpecialModal({
open,
onClose,
@@ -33,6 +67,7 @@ export function StoreSpecialModal({
title,
submitLabel,
isSubmitting = false,
+ error,
}: StoreSpecialModalProps) {
const t = useT()
const { locale } = useLocale()
@@ -42,6 +77,7 @@ export function StoreSpecialModal({
const [closing, setClosing] = useState(false)
const [groupKey, setGroupKey] = useState(initialKey)
const [groupTitle, setGroupTitle] = useState(initialTitle)
+ const [keyTouched, setKeyTouched] = useState(Boolean(initialKey))
useEffect(() => {
if (open) {
@@ -49,6 +85,7 @@ export function StoreSpecialModal({
setClosing(false)
setGroupKey(initialKey)
setGroupTitle(initialTitle)
+ setKeyTouched(Boolean(initialKey))
} else if (mounted) {
setClosing(true)
const timer = setTimeout(() => {
@@ -70,15 +107,22 @@ export function StoreSpecialModal({
if (!mounted) return null
- const trimmedKey = groupKey.trim()
- const trimmedTitle = groupTitle.trim()
- const keyValid = KEY_PATTERN.test(trimmedKey)
- const canSubmit = keyValid && trimmedTitle.length > 0
+ const resolved = resolveGroupFields(groupKey, groupTitle)
+ const canSubmit = resolved !== null
+ const keySlug = slugifyGroupKey(groupKey)
+ const showKeyError = groupKey.trim().length > 0 && !keySlug && !hasNonAscii(groupKey.trim())
+
+ function handleTitleChange(value: string) {
+ setGroupTitle(value)
+ if (!keyTouched) {
+ setGroupKey(slugifyGroupKey(value))
+ }
+ }
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
- if (!canSubmit) return
- onSubmit({ key: trimmedKey, title: trimmedTitle })
+ if (!resolved) return
+ onSubmit(resolved)
}
return createPortal(
@@ -110,33 +154,47 @@ export function StoreSpecialModal({