Polish dashboard page chrome and user access UI.

Rename Customers to Users with FA/EN titles, compact headers and filter bars across apps, and refine manager role copy plus a customer-header link to the business dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-11 00:00:09 +03:30
co-authored by Cursor
parent beec3035e5
commit 6c52ce1247
55 changed files with 395 additions and 122 deletions
@@ -6,7 +6,7 @@
.roleOption {
display: flex;
align-items: center;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: 10px;
@@ -16,17 +16,37 @@
font-family: var(--font-ui);
}
.roleOption input[type='radio'] {
accent-color: var(--primary);
margin-top: 2px;
flex-shrink: 0;
}
.roleOption:has(input:checked) {
border-color: rgba(var(--primary-rgb) / 0.35);
background: rgba(var(--primary-rgb) / 0.08);
}
.roleOptionText {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.roleOptionLabel {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.roleOptionDesc {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
line-height: 1.45;
}
.roleHint {
margin: 12px 0 8px;
font-size: 12px;
@@ -36,12 +56,25 @@
}
.roleListNested {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
margin-top: 4px;
margin-inline-start: 12px;
padding-inline-start: 10px;
border-inline-start: 2px solid rgba(var(--primary-rgb) / 0.2);
}
@media (min-width: 560px) {
.roleListNested {
grid-template-columns: 1fr 1fr;
}
.roleListNested .roleOption:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
}
.roleBadge {
display: inline-block;
margin-top: 4px;
@@ -23,6 +23,12 @@ const TEAM_ROLE_LABEL_KEYS: Record<(typeof ALL_TEAM_ROLES)[number], BusinessMess
viewer: 'customers.access.role.viewer',
}
const TEAM_ROLE_DESC_KEYS: Record<(typeof ALL_TEAM_ROLES)[number], BusinessMessageKey> = {
admin: 'customers.access.role.admin.desc',
editor: 'customers.access.role.editor.desc',
viewer: 'customers.access.role.viewer.desc',
}
export interface ChangeUserAccessModalProps {
open: boolean
user: BusinessCustomerListItem | null
@@ -185,13 +191,13 @@ export function ChangeUserAccessModal({
{access === 'staff' ? (
<>
<p className={styles.roleHint}>
{canAssignAdmin
? t('customers.access.hintSuperAdmin')
: t('customers.access.hintBusiness')}
</p>
{!canAssignAdmin ? (
<p className={styles.roleHint}>{t('customers.access.hintBusiness')}</p>
) : null}
<div className={`${styles.roleList} ${styles.roleListNested}`}>
{teamRoles.map((slug) => (
{teamRoles.map((slug) => {
const role = slug as (typeof ALL_TEAM_ROLES)[number]
return (
<label key={slug} className={styles.roleOption}>
<input
type="radio"
@@ -201,11 +207,17 @@ export function ChangeUserAccessModal({
onChange={() => setTeamRole(slug)}
disabled={submitting}
/>
<span className={styles.roleOptionText}>
<span className={styles.roleOptionLabel}>
{t(TEAM_ROLE_LABEL_KEYS[slug as (typeof ALL_TEAM_ROLES)[number]])}
{t(TEAM_ROLE_LABEL_KEYS[role])}
</span>
<span className={styles.roleOptionDesc}>
{t(TEAM_ROLE_DESC_KEYS[role])}
</span>
</span>
</label>
))}
)
})}
</div>
</>
) : null}
@@ -10,10 +10,7 @@
}
.filtersTitle {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
display: none;
}
.filtersGrid {
@@ -8,14 +8,33 @@
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 36px;
margin-bottom: 12px;
}
.pageTitle {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15em 0.4em;
}
.pageTitleEn {
font-size: 0.52em;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--text-muted);
font-family: var(--font-en), var(--font-ui);
}
.pageTitleEn::before {
content: '•';
margin-inline-end: 0.4em;
letter-spacing: 0;
}
.welcomeTitle {
@@ -26,8 +45,14 @@
}
.pageSubtitle {
display: none;
}
.pageSubtitleContent {
display: block;
font-size: 15px;
color: var(--text-secondary);
margin-top: 6px;
}
.welcomeSubtitle {
@@ -0,0 +1,19 @@
import { useLocale } from '@meshkee/dashboard-ui'
import type { ReactNode } from 'react'
import pageStyles from './PageContent.module.css'
interface PageTitleProps {
children: ReactNode
/** English accent shown after a bullet in FA locale (e.g. USERS). */
en?: string
}
export function PageTitle({ children, en }: PageTitleProps) {
const { locale } = useLocale()
return (
<h2 className={pageStyles.pageTitle}>
{children}
{en && locale === 'fa' ? <span className={pageStyles.pageTitleEn}>{en}</span> : null}
</h2>
)
}
+30 -18
View File
@@ -27,7 +27,7 @@ const en = {
'nav.store.shipping': 'Shipping Fees',
'nav.store.cards': 'Shopping Cards',
'nav.store.settings': 'Settings',
'nav.customers': 'Customers',
'nav.customers': 'Users',
'nav.customerProducts': 'Customer products',
'nav.settings': 'Settings',
'nav.blog': 'Blog',
@@ -158,7 +158,7 @@ const en = {
'home.card.store.desc': 'Manage your store settings, pages and themes.',
'home.card.store.link': 'View store',
'home.card.store.count': 'on sale',
'home.card.customers.title': 'Customers',
'home.card.customers.title': 'Users',
'home.card.customers.desc': 'View and manage your customers and their activity.',
'home.card.customers.link': 'View customers',
'home.card.customers.count': 'people',
@@ -185,7 +185,7 @@ const en = {
'home.chart.orders.error': 'Unable to load order activity.',
'home.chart.orders.bar': '{day}: {count} orders',
'home.chart.orders.cartBar': '{day}: {count} added to basket',
'home.chart.customers.title': 'Customers',
'home.chart.customers.title': 'Users',
'home.chart.customers.subtitle': 'Registrations and active users in the last 30 days',
'home.chart.customers.legend': 'Registered ({count})',
'home.chart.customers.activeLegend': 'Active ({count})',
@@ -766,7 +766,7 @@ const en = {
'storeSettings.error.saveSteps': 'Unable to save order process steps.',
'storeSettings.stepsSaved': 'Order process steps saved.',
'customers.title': 'Customers',
'customers.title': 'Users',
'customers.subtitle':
'View customers who have registered or ordered from your business.',
'customers.filters': 'Filters',
@@ -785,6 +785,7 @@ const en = {
'customers.loading': 'Loading customers...',
'customers.pageMeta': 'Page {page} / {totalPages} · {pageSize} per page · {total} total',
'customers.col.name': 'Name',
'customers.col.role': 'Role',
'customers.col.cell': 'Cell number',
'customers.col.email': 'Email',
'customers.col.orders': 'Orders',
@@ -839,11 +840,16 @@ const en = {
'customers.access.manager': 'Manager',
'customers.access.role.admin': 'Admin',
'customers.access.role.editor': 'Editor',
'customers.access.role.viewer': 'Viewer',
'customers.access.role.viewer': 'Observer',
'customers.access.role.admin.desc':
'Can view and edit content, and also assign editors and observers.',
'customers.access.role.editor.desc': 'Can view content and edit it.',
'customers.access.role.viewer.desc':
'Can only view content. Best for order management and content quality review.',
'customers.access.hintSuperAdmin':
'Admin can manage other managers (except other admins). Editor and viewer manage content only.',
'customers.access.hintBusiness':
'Managers can be Editor or Viewer. Only a super admin can assign Admin.',
'Managers can be Editor or Observer. Only a super admin can assign Admin.',
'customers.access.cancel': 'Cancel',
'customers.access.save': 'Save',
'customers.access.error': 'Unable to update access.',
@@ -853,7 +859,7 @@ const en = {
'customers.access.badge.owner': 'Owner',
'customers.access.badge.admin': 'Admin',
'customers.access.badge.editor': 'Editor',
'customers.access.badge.viewer': 'Viewer',
'customers.access.badge.viewer': 'Observer',
'customers.access.badge.manager': 'Manager',
'customers.toast.accessUpdated': 'Access updated to {role}.',
@@ -1261,7 +1267,7 @@ const en = {
'title.storeItems': 'My Store Items',
'title.orders': 'My Orders',
'title.shoppingCards': 'Shopping Cards',
'title.customers': 'Customers',
'title.customers': 'Users',
'title.customerProducts': 'Customer products',
'title.blog': 'Blog',
'title.myBlogs': 'My Blogs',
@@ -1396,7 +1402,7 @@ const fa: Record<MessageKey, string> = {
'nav.store.shipping': 'هزینه ارسال',
'nav.store.cards': 'کارت‌های خرید',
'nav.store.settings': 'تنظیمات',
'nav.customers': 'مشتریان',
'nav.customers': 'کاربران',
'nav.customerProducts': 'محصولات مشتریان',
'nav.settings': 'تنظیمات',
'nav.blog': 'بلاگ',
@@ -1527,7 +1533,7 @@ const fa: Record<MessageKey, string> = {
'home.card.store.desc': 'تنظیمات، صفحات و ظاهر فروشگاه را مدیریت کنید.',
'home.card.store.link': 'مشاهده فروشگاه',
'home.card.store.count': 'محصول فروش',
'home.card.customers.title': 'مشتریان',
'home.card.customers.title': 'کاربران',
'home.card.customers.desc': 'مشتریان و فعالیت آن‌ها را ببینید و مدیریت کنید.',
'home.card.customers.link': 'مشاهده مشتریان',
'home.card.customers.count': 'نفر',
@@ -1554,7 +1560,7 @@ const fa: Record<MessageKey, string> = {
'home.chart.orders.error': 'بارگذاری فعالیت سفارش‌ها ممکن نشد.',
'home.chart.orders.bar': '{day}: {count} سفارش',
'home.chart.orders.cartBar': '{day}: {count} افزودن به سبد',
'home.chart.customers.title': 'مشتریان',
'home.chart.customers.title': 'کاربران',
'home.chart.customers.subtitle': 'ثبت‌نام و کاربران فعال در ۳۰ روز گذشته',
'home.chart.customers.legend': 'ثبت‌نام ({count})',
'home.chart.customers.activeLegend': 'فعال ({count})',
@@ -2133,7 +2139,7 @@ const fa: Record<MessageKey, string> = {
'storeSettings.error.saveSteps': 'ذخیره مراحل فرآیند سفارش ممکن نشد.',
'storeSettings.stepsSaved': 'مراحل فرآیند سفارش ذخیره شد.',
'customers.title': 'مشتریان',
'customers.title': 'کاربران',
'customers.subtitle':
'مشتریانی را ببینید که در کسب‌وکار شما ثبت‌نام کرده یا سفارش داده‌اند.',
'customers.filters': 'فیلترها',
@@ -2152,6 +2158,7 @@ const fa: Record<MessageKey, string> = {
'customers.loading': 'در حال بارگذاری مشتریان...',
'customers.pageMeta': 'صفحه {page} / {totalPages} · {pageSize} در هر صفحه · {total} کل',
'customers.col.name': 'نام',
'customers.col.role': 'نقش',
'customers.col.cell': 'شماره موبایل',
'customers.col.email': 'ایمیل',
'customers.col.orders': 'سفارش‌ها',
@@ -2206,21 +2213,26 @@ const fa: Record<MessageKey, string> = {
'customers.access.manager': 'مدیر',
'customers.access.role.admin': 'ادمین',
'customers.access.role.editor': 'ویرایشگر',
'customers.access.role.viewer': 'بیننده',
'customers.access.role.viewer': 'ناظر',
'customers.access.role.admin.desc':
'ادمین می‌تواند ببیند، ویرایش کند و همچنین دیگر ویرایشگران و ناظرین را مشخص کند.',
'customers.access.role.editor.desc': 'ویرایشگر می‌تواند محتواها را ببیند و آن‌ها را ویرایش کند.',
'customers.access.role.viewer.desc':
'ناظر می‌تواند فقط محتوا را ببیند. ناظر بیشتر برای مدیریت سفارشات و بررسی کیفی محتواها کاربرد دارد.',
'customers.access.hintSuperAdmin':
'ادمین می‌تواند مدیران دیگر را مدیریت کند (به‌جز ادمین‌های دیگر). ویرایشگر و بیننده فقط محتوا را مدیریت می‌کنند.',
'ادمین می‌تواند مدیران دیگر را مدیریت کند (به‌جز ادمین‌های دیگر). ویرایشگر و ناظر محتوا را مدیریت می‌کنند.',
'customers.access.hintBusiness':
'مدیران می‌توانند ویرایشگر یا بیننده باشند. فقط سوپرادمین می‌تواند ادمین تعیین کند.',
'مدیران می‌توانند ویرایشگر یا ناظر باشند. فقط سوپرادمین می‌تواند ادمین تعیین کند.',
'customers.access.cancel': 'انصراف',
'customers.access.save': 'ذخیره',
'customers.access.error': 'به‌روزرسانی دسترسی ممکن نشد.',
'customers.access.change': 'تغییر دسترسی',
'customers.access.ownerLocked': 'نقش صاحب کسب‌وکار قابل تغییر نیست',
'customers.access.adminLocked': 'فقط سوپرادمین می‌تواند ادمین را تغییر دهد',
'customers.access.badge.owner': 'صاحب',
'customers.access.badge.owner': 'مالک کسب و کار',
'customers.access.badge.admin': 'ادمین',
'customers.access.badge.editor': 'ویرایشگر',
'customers.access.badge.viewer': 'بیننده',
'customers.access.badge.viewer': 'ناظر',
'customers.access.badge.manager': 'مدیر',
'customers.toast.accessUpdated': 'دسترسی به {role} به‌روزرسانی شد.',
@@ -2627,7 +2639,7 @@ const fa: Record<MessageKey, string> = {
'title.storeItems': 'اقلام فروشگاه',
'title.orders': 'سفارش‌های من',
'title.shoppingCards': 'کارت‌های خرید',
'title.customers': 'مشتریان',
'title.customers': 'کاربران',
'title.customerProducts': 'محصولات مشتریان',
'title.blog': 'بلاگ',
'title.myBlogs': 'بلاگ‌های من',
@@ -3,6 +3,7 @@ import { Plus } from 'lucide-react'
import { CategoryModal } from '../components/CategoryModal'
import { BlogCategoryTree } from '../components/BlogCategoryTree'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import type { Category, CategoryFormData } from '../types/category'
import { ApiError } from '../lib/api'
@@ -159,7 +160,7 @@ export function BlogCategoriesPage() {
/>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('blog.categories.title')}</h2>
<PageTitle en="BLOG CATEGORIES">{t('blog.categories.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('blog.categories.subtitle')}</p>
</div>
</div>
+1 -1
View File
@@ -117,7 +117,7 @@ export function BlogDetailsPage() {
</h2>
{blog.abstract ? (
<p
className={[pageStyles.pageSubtitle, abstractLocale.className]
className={[pageStyles.pageSubtitleContent, abstractLocale.className]
.filter(Boolean)
.join(' ')}
lang={abstractLocale.lang}
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FileText, Plus } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { BlogCard } from '../components/BlogCard'
import { BlogCommentsModal } from '../components/BlogCommentsModal'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
@@ -164,7 +165,7 @@ export function BlogListPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.myBlogs')}</h2>
<PageTitle en="BLOGS">{t('title.myBlogs')}</PageTitle>
<p className={pageStyles.pageSubtitle}>
{t('blog.list.subtitle', { count: totalBlogs })}
</p>
+2 -1
View File
@@ -1,6 +1,7 @@
import { FileText, PlusCircle, FolderTree, Settings } from 'lucide-react'
import { SectionCard } from '../components/SectionCard'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import styles from '../components/PageContent.module.css'
@@ -55,7 +56,7 @@ export function BlogPage() {
/>
<div className={styles.pageHeader}>
<div>
<h2 className={styles.pageTitle}>{t('title.blog')}</h2>
<PageTitle en="BLOG">{t('title.blog')}</PageTitle>
<p className={styles.pageSubtitle}>{t('blog.page.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { Switch } from '../components/Switch'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
@@ -83,7 +84,7 @@ export function BlogSettingsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('blog.settings.title')}</h2>
<PageTitle en="BLOG SETTINGS">{t('blog.settings.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('blog.settings.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -4,6 +4,7 @@ import { useToast } from '../context/ToastContext'
import { BrandModal } from '../components/BrandModal'
import { BrandRow } from '../components/BrandRow'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import type { Brand, BrandFormData } from '../types/brand'
import { ApiError } from '../lib/api'
@@ -142,7 +143,7 @@ export function BrandsPage() {
/>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.brands')}</h2>
<PageTitle en="BRANDS">{t('title.brands')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('brands.page.subtitle')}</p>
</div>
</div>
@@ -10,6 +10,7 @@ import {
type AddressListItem,
} from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ImageCropper } from '../components/ImageCropper'
import { MultiSelectDropdown } from '../components/MultiSelectDropdown'
import { RichTextEditor } from '../components/RichTextEditor'
@@ -340,7 +341,7 @@ export function BusinessProfilePage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.businessProfile')}</h2>
<PageTitle en="BUSINESS PROFILE">{t('title.businessProfile')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('bizProfile.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -5,6 +5,7 @@ import { CategoryModal } from '../components/CategoryModal'
import { CategoryAiPromptModal } from '../components/CategoryAiPromptModal'
import { CategoryTree } from '../components/CategoryTree'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { VariationsModal } from '../components/VariationsModal'
import { TechnicalFormModal } from '../components/TechnicalFormModal'
@@ -427,7 +428,7 @@ export function CategoriesPage() {
/>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.categories')}</h2>
<PageTitle en="CATEGORIES">{t('title.categories')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('categories.page.subtitle')}</p>
</div>
</div>
@@ -3,6 +3,7 @@ import { Link, useNavigate, useParams } from 'react-router-dom'
import { ImageOff, MapPin, Pencil, User } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { UserProductStatusModal } from '../components/UserProductStatusModal'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
@@ -197,8 +198,8 @@ export function CustomerProductDetailsPage() {
<div className={styles.headerRow}>
<div>
<h2 className={pageStyles.pageTitle}>{t('customerProducts.detailsTitle')}</h2>
<p className={pageStyles.pageSubtitle}>{t('customerProducts.detailsSubtitle')}</p>
<PageTitle en="CUSTOMER PRODUCT">{t('customerProducts.detailsTitle')}</PageTitle>
<p className={pageStyles.pageSubtitleContent}>{t('customerProducts.detailsSubtitle')}</p>
</div>
<div className={styles.actions}>
<button
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'
import { Package, Plus } from 'lucide-react'
import { Pagination } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { UserProductCard } from '../components/UserProductCard'
import { UserProductStatusModal } from '../components/UserProductStatusModal'
import { useToast } from '../context/ToastContext'
@@ -174,7 +175,7 @@ export function CustomerProductsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('customerProducts.title')}</h2>
<PageTitle en="CUSTOMER PRODUCTS">{t('customerProducts.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>
{t('customerProducts.subtitle', { count: total })}
</p>
@@ -1,11 +1,3 @@
.pageHeaderCompact {
margin-bottom: 12px;
}
.pageHeaderCompact :global(h2) {
margin-bottom: 0;
}
.tablePanel {
margin-top: 12px;
padding: 0;
@@ -26,7 +18,7 @@
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-end;
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
}
@@ -49,23 +41,27 @@
}
.colName {
width: 22%;
width: 18%;
}
.colRole {
width: 16%;
}
.colCell {
width: 14%;
width: 13%;
}
.colOrders {
width: 12%;
width: 10%;
}
.colTransactions {
width: 14%;
width: 12%;
}
.colDate {
width: 12%;
width: 11%;
}
.colActions {
+13 -13
View File
@@ -6,6 +6,7 @@ import { Breadcrumbs } from '../components/Breadcrumbs'
import { ChangeUserAccessModal } from '../components/ChangeUserAccessModal'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { EditCustomerModal } from '../components/EditCustomerModal'
import { PageTitle } from '../components/PageTitle'
import { Pagination } from '../components/Pagination'
import { ToggleSwitch } from '../components/ToggleSwitch'
import { Tooltip } from '../components/Tooltip'
@@ -32,7 +33,7 @@ import accessStyles from '../components/ChangeUserAccessModal.module.css'
import styles from './CustomersPage.module.css'
const PAGE_SIZE = 24
const COLUMN_COUNT = 6
const COLUMN_COUNT = 7
function formatDate(value: string, locale: 'en' | 'fa') {
const d = new Date(value)
@@ -53,13 +54,13 @@ function formatTransactionTotal(total: number | null | undefined) {
return formatIrtPrice(total)
}
function accessBadgeKey(user: BusinessCustomerListItem): BusinessMessageKey | null {
function accessBadgeKey(user: BusinessCustomerListItem): BusinessMessageKey {
if (user.isBusinessOwner) return 'customers.access.badge.owner'
if (user.teamRole === 'admin') return 'customers.access.badge.admin'
if (user.teamRole === 'editor') return 'customers.access.badge.editor'
if (user.teamRole === 'viewer') return 'customers.access.badge.viewer'
if (user.businessMemberId) return 'customers.access.badge.manager'
return null
return 'customers.access.customer'
}
function teamRoleMessageKey(teamRole: string): BusinessMessageKey {
@@ -310,15 +311,13 @@ export function CustomersPage() {
<main className={pageStyles.content}>
<Breadcrumbs
items={[
{ label: 'Home', href: '/' },
{ label: 'Customers' },
{ label: t('nav.home'), href: '/' },
{ label: t('customers.title') },
]}
/>
<div className={`${pageStyles.pageHeader} ${styles.pageHeaderCompact}`}>
<div>
<h2 className={pageStyles.pageTitle}>{t('customers.title')}</h2>
</div>
<div className={pageStyles.pageHeader}>
<PageTitle en="USERS">{t('customers.title')}</PageTitle>
</div>
<div className={filterStyles.filtersPanel}>
@@ -384,7 +383,6 @@ export function CustomersPage() {
<div className={styles.tablePanel}>
<div className={styles.tableWrap}>
<div className={styles.tableHeader}>
<div className={styles.tableHeaderTitle}>{t('customers.listTitle')}</div>
<div className={styles.meta}>
{data ? (
data.total > 0 ? (
@@ -410,6 +408,7 @@ export function CustomersPage() {
>
<colgroup>
<col className={styles.colName} />
<col className={styles.colRole} />
<col className={styles.colCell} />
<col className={styles.colOrders} />
<col className={styles.colTransactions} />
@@ -419,6 +418,7 @@ export function CustomersPage() {
<thead>
<tr>
<th className={styles.th}>{t('customers.col.name')}</th>
<th className={styles.th}>{t('customers.col.role')}</th>
<th className={styles.th}>{t('customers.col.cell')}</th>
<th className={styles.th}>{t('customers.col.orders')}</th>
<th className={styles.th}>{t('customers.col.transactions')}</th>
@@ -468,13 +468,13 @@ export function CustomersPage() {
>
{name}
</div>
{badgeKey ? (
<span className={accessStyles.roleBadge}>{t(badgeKey)}</span>
) : null}
{!customer.isEnabled && (
<div className={styles.statusDisabled}>{t('customers.disabled')}</div>
)}
</td>
<td className={styles.td}>
<span className={accessStyles.roleBadge}>{t(badgeKey)}</span>
</td>
<td className={styles.td}>{formatCellForDisplay(customer.cellNumber)}</td>
<td className={styles.td}>
{customer.orderCount == null || customer.orderCount === 0 ? (
+2 -1
View File
@@ -9,6 +9,7 @@ import { ProductVariantsModal } from '../components/ProductVariantsModal'
import { ProductTechnicalInfoModal } from '../components/ProductTechnicalInfoModal'
import { ProductCommentsModal } from '../components/ProductCommentsModal'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { useT } from '../i18n/useT'
import { ApiError } from '../lib/api'
import {
@@ -201,7 +202,7 @@ export function MyProductsPage() {
/>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.myProducts')}</h2>
<PageTitle en="PRODUCTS">{t('title.myProducts')}</PageTitle>
<p className={pageStyles.pageSubtitle}>
{t('products.list.subtitle', { count: totalProducts })}
</p>
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { CreditCard, Eye, RotateCcw, Search, Trash2 } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { OrderItemsModal } from '../components/OrderItemsModal'
import { OrderStepModal } from '../components/OrderStepModal'
@@ -314,7 +315,7 @@ export function OrdersPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('orders.title')}</h2>
<PageTitle en="ORDERS">{t('orders.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('orders.subtitle')}</p>
</div>
</div>
@@ -3,6 +3,7 @@ import { Plus } from 'lucide-react'
import { CategoryModal } from '../components/CategoryModal'
import { BlogCategoryTree } from '../components/BlogCategoryTree'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import type { Category, CategoryFormData } from '../types/category'
import { ApiError } from '../lib/api'
@@ -159,7 +160,7 @@ export function PortfolioCategoriesPage() {
/>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('portfolio.categories.title')}</h2>
<PageTitle en="PORTFOLIO CATEGORIES">{t('portfolio.categories.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('portfolio.categories.subtitle')}</p>
</div>
</div>
@@ -101,7 +101,7 @@ export function PortfolioDetailsPage() {
<div>
<h2 className={pageStyles.pageTitle}>{portfolio.title}</h2>
{portfolio.abstract && (
<p className={pageStyles.pageSubtitle}>{portfolio.abstract}</p>
<p className={pageStyles.pageSubtitleContent}>{portfolio.abstract}</p>
)}
</div>
</div>
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Plus } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { PortfolioCard } from '../components/PortfolioCard'
import { PortfolioCommentsModal } from '../components/PortfolioCommentsModal'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
@@ -172,7 +173,7 @@ export function PortfolioListPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.myPortfolios')}</h2>
<PageTitle en="PORTFOLIOS">{t('title.myPortfolios')}</PageTitle>
<p className={pageStyles.pageSubtitle}>
{t('portfolio.list.subtitle', { count: totalPortfolios })}
</p>
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { Switch } from '../components/Switch'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
@@ -83,7 +84,7 @@ export function PortfolioSettingsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('portfolio.settings.title')}</h2>
<PageTitle en="PORTFOLIO SETTINGS">{t('portfolio.settings.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('portfolio.settings.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -1,6 +1,7 @@
import { Briefcase, PlusCircle, FolderTree, Settings } from 'lucide-react'
import { SectionCard } from '../components/SectionCard'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import styles from '../components/PageContent.module.css'
@@ -55,7 +56,7 @@ export function PortfoliosPage() {
/>
<div className={styles.pageHeader}>
<div>
<h2 className={styles.pageTitle}>{t('title.portfolios')}</h2>
<PageTitle en="PORTFOLIOS">{t('title.portfolios')}</PageTitle>
<p className={styles.pageSubtitle}>{t('portfolio.page.subtitle')}</p>
</div>
</div>
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { Switch } from '../components/Switch'
import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
@@ -104,7 +105,7 @@ export function ProductSettingsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('productSettings.title')}</h2>
<PageTitle en="PRODUCT SETTINGS">{t('productSettings.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('productSettings.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -2,6 +2,7 @@ import { FolderTree, PlusCircle, Package, Settings, Tag } from 'lucide-react'
import { SectionCard } from '../components/SectionCard'
import { ProductActivityChart } from '../components/ProductActivityChart'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import styles from '../components/PageContent.module.css'
@@ -57,7 +58,7 @@ export function ProductsPage() {
/>
<div className={styles.pageHeader}>
<div>
<h2 className={styles.pageTitle}>{t('title.products')}</h2>
<PageTitle en="PRODUCTS">{t('title.products')}</PageTitle>
<p className={styles.pageSubtitle}>{t('products.overview.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -10,6 +10,7 @@ import {
type AddressListItem,
} from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import pageStyles from '../components/PageContent.module.css'
import { useAuth } from '../context/AuthContext'
import { useToast } from '../context/ToastContext'
@@ -289,7 +290,7 @@ export function ProfilePage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('profile.title')}</h2>
<PageTitle en="PROFILE">{t('profile.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('profile.subtitle')}</p>
</div>
</div>
@@ -3,6 +3,7 @@ import { Play, RotateCcw, Search, Trash2 } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useLocale } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { Pagination } from '../components/Pagination'
import { Tooltip } from '../components/Tooltip'
@@ -202,7 +203,7 @@ export function ShoppingCardsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('shoppingCards.title')}</h2>
<PageTitle en="SHOPPING CARDS">{t('shoppingCards.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('shoppingCards.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Plus, RotateCcw, Search } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { CreateStoreItemsModal } from '../components/CreateStoreItemsModal'
import { EditStoreItemsModal } from '../components/EditStoreItemsModal'
@@ -282,7 +283,7 @@ function StoreItemsPageContent() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.storeItems')}</h2>
<PageTitle en="STORE ITEMS">{t('title.storeItems')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('storeItems.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -7,6 +7,7 @@ import {
} from 'lucide-react'
import { SectionCard } from '../components/SectionCard'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import styles from '../components/PageContent.module.css'
@@ -62,7 +63,7 @@ export function StorePage() {
/>
<div className={styles.pageHeader}>
<div>
<h2 className={styles.pageTitle}>{t('title.store')}</h2>
<PageTitle en="STORE">{t('title.store')}</PageTitle>
<p className={styles.pageSubtitle}>{t('store.page.subtitle')}</p>
</div>
</div>
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { StepColorPicker } from '../components/StepColorPicker'
import { Switch } from '../components/Switch'
import { Tooltip } from '../components/Tooltip'
@@ -215,7 +216,7 @@ export function StoreSettingsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('storeSettings.title')}</h2>
<PageTitle en="STORE SETTINGS">{t('storeSettings.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('storeSettings.subtitle')}</p>
</div>
</div>
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { Plus } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { EditStoreItemsModal } from '../components/EditStoreItemsModal'
import { PickStoreSpecialItemsModal } from '../components/PickStoreSpecialItemsModal'
@@ -311,7 +312,7 @@ export function StoreSpecialsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.specialItems')}</h2>
<PageTitle en="SPECIAL ITEMS">{t('title.specialItems')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('website.specialItems.subtitle')}</p>
</div>
</div>
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { RotateCcw, Search } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ContactSubmissionDetailModal } from '../components/ContactSubmissionDetailModal'
import { Pagination } from '../components/Pagination'
import { useT } from '../i18n/useT'
@@ -112,7 +113,7 @@ export function WebsiteContactPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.contactForm')}</h2>
<PageTitle en="CONTACT">{t('title.contactForm')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('website.contact.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -11,6 +11,7 @@ import {
} from 'lucide-react'
import { SectionCard } from '../components/SectionCard'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import styles from '../components/PageContent.module.css'
@@ -100,7 +101,7 @@ export function WebsitePage() {
/>
<div className={styles.pageHeader}>
<div>
<h2 className={styles.pageTitle}>{t('title.website')}</h2>
<PageTitle en="WEBSITE">{t('title.website')}</PageTitle>
<p className={styles.pageSubtitle}>{t('website.page.subtitle')}</p>
</div>
</div>
@@ -4,6 +4,7 @@ import {
type WebsiteSliderSlideFormData,
} from '../components/AddWebsiteSliderSlideModal'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { WebsiteSliderGallery } from '../components/WebsiteSliderGallery'
import { useToast } from '../context/ToastContext'
@@ -138,7 +139,7 @@ export function WebsiteSlidersPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.sliders')}</h2>
<PageTitle en="SLIDERS">{t('title.sliders')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('website.sliders.subtitle')}</p>
</div>
</div>
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Plus } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { PickWebsiteBrandsModal } from '../components/PickWebsiteBrandsModal'
import { StoreSpecialModal } from '../components/StoreSpecialModal'
@@ -204,7 +205,7 @@ export function WebsiteSpecialBrandsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.specialBrands')}</h2>
<PageTitle en="SPECIAL BRANDS">{t('title.specialBrands')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('website.specialBrands.subtitle')}</p>
</div>
</div>
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Plus } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { PickWebsiteCategoriesModal } from '../components/PickWebsiteCategoriesModal'
import { StoreSpecialModal } from '../components/StoreSpecialModal'
@@ -204,7 +205,7 @@ export function WebsiteSpecialCategoriesPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('title.specialCategories')}</h2>
<PageTitle en="SPECIAL CATEGORIES">{t('title.specialCategories')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('website.specialCategories.subtitle')}</p>
</div>
</div>
@@ -122,6 +122,28 @@
.profileWrap {
position: relative;
display: flex;
align-items: center;
gap: 8px;
}
.businessDashBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
color: var(--primary);
background: rgba(var(--primary-rgb) / 0.1);
border: 1px solid rgba(var(--primary-rgb) / 0.18);
transition: background 0.15s, transform 0.15s, box-shadow 0.15s;
}
.businessDashBtn:hover {
background: rgba(var(--primary-rgb) / 0.16);
box-shadow: 0 4px 12px rgba(var(--primary-rgb) / 0.18);
transform: translateY(-1px);
}
.profile {
+26 -1
View File
@@ -1,10 +1,12 @@
import { useEffect, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Menu, Bell, MessageSquare, ChevronDown, User, KeyRound, LogOut } from 'lucide-react'
import { Menu, Bell, MessageSquare, ChevronDown, User, KeyRound, LogOut, LayoutDashboard } from 'lucide-react'
import { LanguageSelect, PasswordResetModal, useLocale } from '@meshkee/dashboard-ui'
import { useAuth } from '../context/AuthContext'
import { useTenantBranding } from '../context/TenantBrandingContext'
import { useT } from '../i18n/useT'
import { getActiveBusinessId } from '../lib/businessContext'
import { getBusinessDashboardUrl } from '../lib/config'
import { changePassword } from '../services/authService'
import meshkeeLogo from '../assets/meshkee-logo.png'
import styles from './Header.module.css'
@@ -57,6 +59,17 @@ export function Header({ hideMenu = false, showBrand = false }: HeaderProps) {
? (businessName && businessName !== brandTitle ? businessName : '')
: (businessNameEn && businessNameEn !== brandTitle ? businessNameEn : '')
const activeBusinessId = getActiveBusinessId()
const canOpenBusinessDashboard = Boolean(
user?.isSuperAdmin ||
user?.roles.includes('super_admin') ||
user?.businesses.some(
(business) =>
String(business.id) === String(activeBusinessId) &&
(business.isOwner || Boolean(business.teamRole)),
),
)
useEffect(() => {
if (!menuOpen) return
@@ -128,6 +141,18 @@ export function Header({ hideMenu = false, showBrand = false }: HeaderProps) {
</button>
<div className={styles.profileWrap} ref={menuRef}>
{canOpenBusinessDashboard ? (
<a
className={styles.businessDashBtn}
href={getBusinessDashboardUrl()}
target="_blank"
rel="noopener noreferrer"
aria-label={t('header.openBusinessDashboard')}
title={t('header.openBusinessDashboard')}
>
<LayoutDashboard size={18} />
</a>
) : null}
<button
type="button"
className={`${styles.profile} ${menuOpen ? styles.profileOpen : ''}`}
@@ -8,19 +8,44 @@
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 36px;
margin-bottom: 12px;
}
.pageTitle {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15em 0.4em;
}
.pageTitleEn {
font-size: 0.52em;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--text-muted);
font-family: var(--font-en), var(--font-ui);
}
.pageTitleEn::before {
content: '•';
margin-inline-end: 0.4em;
letter-spacing: 0;
}
.pageSubtitle {
display: none;
}
.pageSubtitleContent {
display: block;
font-size: 15px;
color: var(--text-secondary);
margin-top: 6px;
}
.dateBadge {
@@ -0,0 +1,19 @@
import { useLocale } from '@meshkee/dashboard-ui'
import type { ReactNode } from 'react'
import pageStyles from './PageContent.module.css'
interface PageTitleProps {
children: ReactNode
/** English accent shown after a bullet in FA locale (e.g. USERS). */
en?: string
}
export function PageTitle({ children, en }: PageTitleProps) {
const { locale } = useLocale()
return (
<h2 className={pageStyles.pageTitle}>
{children}
{en && locale === 'fa' ? <span className={pageStyles.pageTitleEn}>{en}</span> : null}
</h2>
)
}
+2
View File
@@ -21,6 +21,7 @@ const en = {
'header.notifications': 'Notifications',
'header.changePassword': 'Change password',
'header.myProfile': 'My Profile',
'header.openBusinessDashboard': 'Open business dashboard',
'home.welcome': 'Welcome, dear {name}.',
'home.welcomeFallback': 'there',
@@ -406,6 +407,7 @@ const fa: Record<MessageKey, string> = {
'header.notifications': 'اعلان‌ها',
'header.changePassword': 'تغییر رمز عبور',
'header.myProfile': 'پروفایل من',
'header.openBusinessDashboard': 'رفتن به داشبورد کسب‌وکار',
'home.welcome': '{name} عزیز، خوش آمدی.',
'home.welcomeFallback': 'کاربر',
+10
View File
@@ -1,6 +1,7 @@
import {
CUSTOMER_SUBDOMAIN_PREFIX,
getBaseDomainFromHost,
getBusinessDashboardHost,
getCustomerDashboardHost,
isCustomerDashboardHost,
} from '@meshkee/dashboard-core'
@@ -31,3 +32,12 @@ export function isAllowedCustomerHost(hostname = window.location.hostname): bool
export function getTenantDomain(): string {
return getBaseBusinessDomain()
}
/** Open the matching business dashboard (local: port 5173). */
export function getBusinessDashboardUrl(): string {
const host = getBusinessDashboardHost(getBaseBusinessDomain())
const { protocol, port } = window.location
const businessPort = port === '5175' ? '5173' : port
const portSuffix = businessPort ? `:${businessPort}` : ''
return `${protocol}//${host}${portSuffix}/`
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { PageTitle } from '../components/PageTitle'
import { useCallback, useEffect, useState } from 'react'
import { MapPin, Pencil, Plus, Trash2 } from 'lucide-react'
import { Breadcrumbs, useToast } from '@meshkee/dashboard-ui'
@@ -107,7 +108,7 @@ export function AddressesPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('addresses.title')}</h2>
<PageTitle en="ADDRESSES">{t('addresses.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('addresses.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -1,3 +1,4 @@
import { PageTitle } from '../components/PageTitle'
import { useEffect, useState } from 'react'
import { Heart } from 'lucide-react'
import { Breadcrumbs, Pagination, useToast } from '@meshkee/dashboard-ui'
@@ -82,7 +83,7 @@ export function FavoritesPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('favorites.title')}</h2>
<PageTitle en="FAVORITES">{t('favorites.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('favorites.subtitle')}</p>
</div>
</div>
@@ -1,3 +1,4 @@
import { PageTitle } from '../components/PageTitle'
import { useEffect, useMemo, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { ImageOff, MapPin, Pencil } from 'lucide-react'
@@ -172,8 +173,8 @@ export function MyProductDetailsPage() {
<div className={styles.headerRow}>
<div>
<h2 className={pageStyles.pageTitle}>{t('myProducts.detailsTitle')}</h2>
<p className={pageStyles.pageSubtitle}>{t('myProducts.detailsSubtitle')}</p>
<PageTitle en="PRODUCT">{t('myProducts.detailsTitle')}</PageTitle>
<p className={pageStyles.pageSubtitleContent}>{t('myProducts.detailsSubtitle')}</p>
</div>
<button
type="button"
+2 -1
View File
@@ -1,3 +1,4 @@
import { PageTitle } from '../components/PageTitle'
import { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Package, Plus } from 'lucide-react'
@@ -126,7 +127,7 @@ export function MyProductsPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('myProducts.title')}</h2>
<PageTitle en="MY PRODUCTS">{t('myProducts.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('myProducts.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -1,3 +1,4 @@
import { PageTitle } from '../components/PageTitle'
import { useEffect, useMemo, useState } from 'react'
import { Breadcrumbs, Pagination, useLocale } from '@meshkee/dashboard-ui'
import { OrderItemsModal } from '../components/OrderItemsModal'
@@ -71,7 +72,7 @@ export function OrdersPage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('orders.title')}</h2>
<PageTitle en="ORDERS">{t('orders.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('orders.subtitle')}</p>
</div>
</div>
+2 -1
View File
@@ -1,3 +1,4 @@
import { PageTitle } from '../components/PageTitle'
import { useEffect, useState } from 'react'
import { Breadcrumbs, useToast } from '@meshkee/dashboard-ui'
import { useAuth } from '../context/AuthContext'
@@ -81,7 +82,7 @@ export function ProfilePage() {
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>{t('profile.title')}</h2>
<PageTitle en="PROFILE">{t('profile.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('profile.subtitle')}</p>
</div>
</div>
@@ -8,19 +8,44 @@
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
margin-bottom: 12px;
}
.pageTitle {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15em 0.4em;
}
.pageTitleEn {
font-size: 0.52em;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--text-secondary);
font-family: var(--font-en), var(--font-ui);
}
.pageTitleEn::before {
content: '•';
margin-inline-end: 0.4em;
letter-spacing: 0;
}
.pageSubtitle {
display: none;
}
.pageSubtitleContent {
display: block;
font-size: 14px;
color: var(--text-secondary);
margin-top: 6px;
}
.dateBadge {
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react'
import pageStyles from './PageContent.module.css'
interface PageTitleProps {
children: ReactNode
/** English accent shown after a bullet (e.g. USERS). */
en?: string
}
export function PageTitle({ children, en }: PageTitleProps) {
return (
<h2 className={pageStyles.pageTitle}>
{children}
{en ? <span className={pageStyles.pageTitleEn}>{en}</span> : null}
</h2>
)
}
@@ -9,10 +9,7 @@
}
.filtersTitle {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 10px;
display: none;
}
.filtersGrid {
@@ -68,6 +68,10 @@
cursor: pointer;
}
.roleOption input[type='radio'] {
accent-color: var(--primary);
}
.roleOption:has(input:checked) {
border-color: rgba(var(--primary-rgb) / 0.35);
background: rgba(var(--primary-rgb) / 0.08);
+2 -3
View File
@@ -46,7 +46,7 @@ const TEAM_ROLE_ORDER = ['admin', 'editor', 'viewer'] as const
const TEAM_ROLE_LABELS: Record<string, string> = {
admin: 'Admin',
editor: 'Editor',
viewer: 'Viewer',
viewer: 'Observer',
}
function teamRoleLabel(slug: string | null | undefined, teamRoles: RoleOption[]) {
@@ -1021,8 +1021,7 @@ export function UsersPage() {
{businessAccess === 'staff' ? (
<>
<p className={styles.roleHint}>
Only a super admin can assign Admin. Admins can assign Editor or Viewer. Admins
cannot change other admins.
Only a super admin can assign Admin. Admins can assign Editor or Observer.
</p>
<div className={`${styles.roleList} ${styles.roleListNested}`}>
{orderedTeamRoles.map((role) => (