Rename store items to store products and add an empty-state CTA.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-15 14:56:08 +03:30
co-authored by Cursor
parent 8e8929050e
commit 9a59bc8f65
3 changed files with 130 additions and 23 deletions
+37 -20
View File
@@ -662,7 +662,8 @@ const en = {
'storeItems.search': 'Search', 'storeItems.search': 'Search',
'storeItems.clearFilters': 'Clear filters', 'storeItems.clearFilters': 'Clear filters',
'storeItems.loading': 'Loading store items...', 'storeItems.loading': 'Loading store items...',
'storeItems.empty': 'No store items yet. Use the + button to add products from your catalog.', 'storeItems.empty': "You don't have any products for sale yet.",
'storeItems.emptyHint': 'Add products from your catalog to start selling.',
'storeItems.emptyFiltered': 'No store items match your filters.', 'storeItems.emptyFiltered': 'No store items match your filters.',
'storeItems.pagination': 'storeItems.pagination':
'Page {page} / {totalPages} · {perPage} per page · {total} total', 'Page {page} / {totalPages} · {perPage} per page · {total} total',
@@ -1471,11 +1472,17 @@ const en = {
'website.ePayment.gateway.digipay': 'DigiPay', 'website.ePayment.gateway.digipay': 'DigiPay',
'website.ePayment.gateway.digipayDesc': 'DigiPay checkout — coming soon.', 'website.ePayment.gateway.digipayDesc': 'DigiPay checkout — coming soon.',
'website.ePayment.gateway.zarinpal': 'Zarinpal', 'website.ePayment.gateway.zarinpal': 'Zarinpal',
'website.ePayment.gateway.zarinpalDesc': 'Zarinpal gateway — coming soon.', 'website.ePayment.gateway.zarinpalDesc':
'Enter your Merchant ID from the ZarinPal panel. Customers can choose ZarinPal or Mellat at checkout when both are enabled.',
'website.ePayment.comingSoon': 'Coming soon', 'website.ePayment.comingSoon': 'Coming soon',
'website.ePayment.terminalId': 'Terminal ID', 'website.ePayment.terminalId': 'Terminal ID',
'website.ePayment.username': 'Username', 'website.ePayment.username': 'Username',
'website.ePayment.password': 'Password', 'website.ePayment.password': 'Password',
'website.ePayment.merchantId': 'Merchant ID',
'website.ePayment.merchantIdPlaceholder': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'website.ePayment.sandbox': 'Sandbox mode',
'website.ePayment.sandboxDesc':
'Use ZarinPal sandbox endpoints for testing. Turn off for live payments.',
'website.ePayment.passwordSet': 'saved', 'website.ePayment.passwordSet': 'saved',
'website.ePayment.passwordPlaceholder': 'Enter gateway password', 'website.ePayment.passwordPlaceholder': 'Enter gateway password',
'website.ePayment.passwordPlaceholderSet': 'Leave blank to keep current password', 'website.ePayment.passwordPlaceholderSet': 'Leave blank to keep current password',
@@ -1484,6 +1491,7 @@ const en = {
'website.ePayment.saveCredentials': 'Save credentials', 'website.ePayment.saveCredentials': 'Save credentials',
'website.ePayment.error.credentials': 'Terminal ID and username are required when Mellat is enabled.', 'website.ePayment.error.credentials': 'Terminal ID and username are required when Mellat is enabled.',
'website.ePayment.error.passwordRequired': 'Password is required the first time you enable Mellat.', 'website.ePayment.error.passwordRequired': 'Password is required the first time you enable Mellat.',
'website.ePayment.error.zarinpalMerchant': 'Merchant ID is required when ZarinPal is enabled.',
'title.signIn': 'Sign in', 'title.signIn': 'Sign in',
'title.home': 'Home', 'title.home': 'Home',
@@ -1859,7 +1867,7 @@ const fa: Record<MessageKey, string> = {
'nav.products.settings': 'تنظیمات', 'nav.products.settings': 'تنظیمات',
'nav.store': 'فروشگاه', 'nav.store': 'فروشگاه',
'nav.store.overview': 'نمای کلی', 'nav.store.overview': 'نمای کلی',
'nav.store.items': 'اقلام فروشگاه', 'nav.store.items': 'محصولات فروشگاه',
'nav.store.orders': 'سفارش‌های من', 'nav.store.orders': 'سفارش‌های من',
'nav.store.shipping': 'هزینه ارسال', 'nav.store.shipping': 'هزینه ارسال',
'nav.store.cards': 'کارت‌های خرید', 'nav.store.cards': 'کارت‌های خرید',
@@ -2474,9 +2482,9 @@ const fa: Record<MessageKey, string> = {
'productSettings.reviewsAutoDesc': 'productSettings.reviewsAutoDesc':
'بررسی‌های کارشناسی جدید بلافاصله پس از ثبت منتشر می‌شوند. در صورت نیاز بعداً می‌توانید آن‌ها را رد کنید.', 'بررسی‌های کارشناسی جدید بلافاصله پس از ثبت منتشر می‌شوند. در صورت نیاز بعداً می‌توانید آن‌ها را رد کنید.',
'store.page.subtitle': 'اقلام، سفارش‌ها و تنظیمات فروشگاه را مدیریت کنید.', 'store.page.subtitle': 'محصولات، سفارش‌ها و تنظیمات فروشگاه را مدیریت کنید.',
'store.card.items.desc': 'همه اقلام فهرست‌شده در فروشگاه را ببینید و مدیریت کنید.', 'store.card.items.desc': 'همه محصولات فهرست‌شده در فروشگاه را ببینید و مدیریت کنید.',
'store.card.items.link': 'مشاهده اقلام', 'store.card.items.link': 'مشاهده محصولات',
'store.card.orders.desc': 'سفارش‌های مشتریان و ارسال را پیگیری و مدیریت کنید.', 'store.card.orders.desc': 'سفارش‌های مشتریان و ارسال را پیگیری و مدیریت کنید.',
'store.card.orders.link': 'مشاهده سفارش‌ها', 'store.card.orders.link': 'مشاهده سفارش‌ها',
'store.card.shipping.desc': 'نرخ‌ها، مناطق و گزینه‌های ارسال را پیکربندی کنید.', 'store.card.shipping.desc': 'نرخ‌ها، مناطق و گزینه‌های ارسال را پیکربندی کنید.',
@@ -2495,14 +2503,15 @@ const fa: Record<MessageKey, string> = {
'storeItems.onlyDiscounted': 'فقط تخفیف‌دار', 'storeItems.onlyDiscounted': 'فقط تخفیف‌دار',
'storeItems.search': 'جستجو', 'storeItems.search': 'جستجو',
'storeItems.clearFilters': 'پاک کردن فیلترها', 'storeItems.clearFilters': 'پاک کردن فیلترها',
'storeItems.loading': 'در حال بارگذاری اقلام فروشگاه...', 'storeItems.loading': 'در حال بارگذاری محصولات فروشگاه...',
'storeItems.empty': 'هنوز قلمی در فروشگاه نیست. با دکمه + از کاتالوگ اضافه کنید.', 'storeItems.empty': 'هنوز هیچ محصولی برای فروش ندارید',
'storeItems.emptyFiltered': 'هیچ قلمی با فیلترهای شما مطابقت ندارد.', 'storeItems.emptyHint': 'از کاتالوگ محصول اضافه کنید.',
'storeItems.emptyFiltered': 'هیچ محصولی با فیلترهای شما مطابقت ندارد.',
'storeItems.pagination': 'storeItems.pagination':
'صفحه {page} / {totalPages} · {perPage} در هر صفحه · {total} مجموع', 'صفحه {page} / {totalPages} · {perPage} در هر صفحه · {total} مجموع',
'storeItems.errorLoad': 'بارگذاری اقلام فروشگاه ممکن نشد.', 'storeItems.errorLoad': 'بارگذاری محصولات فروشگاه ممکن نشد.',
'storeItems.errorRemove': 'حذف قلم از فروشگاه ممکن نشد.', 'storeItems.errorRemove': 'حذف محصول از فروشگاه ممکن نشد.',
'storeItems.add': 'افزودن اقلام فروشگاه', 'storeItems.add': 'افزودن محصول به فروشگاه',
'storeItems.cartOpen': 'باز کردن سبد خرید، {count} قلم', 'storeItems.cartOpen': 'باز کردن سبد خرید، {count} قلم',
'storeItems.cartOne': 'قلم در سبد', 'storeItems.cartOne': 'قلم در سبد',
'storeItems.cartMany': 'قلم در سبد', 'storeItems.cartMany': 'قلم در سبد',
@@ -2521,7 +2530,7 @@ const fa: Record<MessageKey, string> = {
'storeItems.card.addToCart': 'افزودن به سبد خرید', 'storeItems.card.addToCart': 'افزودن به سبد خرید',
'storeItems.pick.title': 'انتخاب تنوع', 'storeItems.pick.title': 'انتخاب تنوع',
'storeItems.pick.inStock': '{count} موجود', 'storeItems.pick.inStock': '{count} موجود',
'storeItems.create.title': 'افزودن اقلام فروشگاه', 'storeItems.create.title': 'افزودن محصول به فروشگاه',
'storeItems.create.subtitle': 'تنوع‌های قابل فروش را از تنوع‌های یک محصول بسازید.', 'storeItems.create.subtitle': 'تنوع‌های قابل فروش را از تنوع‌های یک محصول بسازید.',
'storeItems.create.productPlaceholder': 'محصول — حداقل ۳ کاراکتر برای جستجو', 'storeItems.create.productPlaceholder': 'محصول — حداقل ۳ کاراکتر برای جستجو',
'storeItems.create.loadingVariations': 'در حال بارگذاری تنوع‌های محصول...', 'storeItems.create.loadingVariations': 'در حال بارگذاری تنوع‌های محصول...',
@@ -2533,7 +2542,7 @@ const fa: Record<MessageKey, string> = {
'storeItems.create.addAllProducts': 'افزودن همه محصولات', 'storeItems.create.addAllProducts': 'افزودن همه محصولات',
'storeItems.create.addingAll': 'در حال ایجاد برای همه محصولات…', 'storeItems.create.addingAll': 'در حال ایجاد برای همه محصولات…',
'storeItems.create.addAllDone': 'ایجاد شد {created}، رد شد {skipped}، ناموفق {failed}', 'storeItems.create.addAllDone': 'ایجاد شد {created}، رد شد {skipped}، ناموفق {failed}',
'storeItems.create.addAllConfirmTitle': 'افزودن اقلام برای همه محصولات؟', 'storeItems.create.addAllConfirmTitle': 'افزودن همه محصولات به فروشگاه؟',
'storeItems.create.addAllConfirmIntro': 'storeItems.create.addAllConfirmIntro':
'برای هر محصولی که هنوز به فروشگاه اضافه نشده است، تنوع‌های قابل فروش ساخته می‌شود. فقط وقتی می‌خواهید همهٔ محصولات یک‌جا به فروشگاه اضافه شود از این گزینه استفاده کنید.', 'برای هر محصولی که هنوز به فروشگاه اضافه نشده است، تنوع‌های قابل فروش ساخته می‌شود. فقط وقتی می‌خواهید همهٔ محصولات یک‌جا به فروشگاه اضافه شود از این گزینه استفاده کنید.',
'storeItems.create.addAllConfirmMax': 'storeItems.create.addAllConfirmMax':
@@ -2547,16 +2556,16 @@ const fa: Record<MessageKey, string> = {
'storeItems.create.addAllResultTitle': 'ایجاد گروهی تمام شد', 'storeItems.create.addAllResultTitle': 'ایجاد گروهی تمام شد',
'storeItems.create.addAllDoneBtn': 'باشه', 'storeItems.create.addAllDoneBtn': 'باشه',
'storeItems.create.cancel': 'انصراف', 'storeItems.create.cancel': 'انصراف',
'storeItems.create.submit': 'ایجاد اقلام فروشگاه', 'storeItems.create.submit': 'افزودن محصول به فروشگاه',
'storeItems.create.submitting': 'در حال ایجاد…', 'storeItems.create.submitting': 'در حال افزودن…',
'storeItems.create.errorProducts': 'بارگذاری محصولات ممکن نشد.', 'storeItems.create.errorProducts': 'بارگذاری محصولات ممکن نشد.',
'storeItems.create.errorVariations': 'بارگذاری تنوع‌های محصول ممکن نشد.', 'storeItems.create.errorVariations': 'بارگذاری تنوع‌های محصول ممکن نشد.',
'storeItems.create.errorCreate': 'ایجاد اقلام فروشگاه ممکن نشد.', 'storeItems.create.errorCreate': 'افزودن محصول به فروشگاه ممکن نشد.',
'storeItems.price.contact': 'برای قیمت، تماس بگیرید', 'storeItems.price.contact': 'برای قیمت، تماس بگیرید',
'storeItems.edit.title': 'ویرایش محصول فروشگاه', 'storeItems.edit.title': 'ویرایش محصول فروشگاه',
'storeItems.edit.save': 'ذخیره تغییرات', 'storeItems.edit.save': 'ذخیره تغییرات',
'storeItems.edit.saving': 'در حال ذخیره…', 'storeItems.edit.saving': 'در حال ذخیره…',
'storeItems.edit.errorSave': 'ذخیره اقلام فروشگاه ممکن نشد.', 'storeItems.edit.errorSave': 'ذخیره محصول فروشگاه ممکن نشد.',
'storeItems.search.clear': 'پاک کردن انتخاب', 'storeItems.search.clear': 'پاک کردن انتخاب',
'storeItems.search.noProducts': 'محصولی موجود نیست.', 'storeItems.search.noProducts': 'محصولی موجود نیست.',
'storeItems.search.noMatch': 'محصولی یافت نشد.', 'storeItems.search.noMatch': 'محصولی یافت نشد.',
@@ -3303,11 +3312,17 @@ const fa: Record<MessageKey, string> = {
'website.ePayment.gateway.digipay': 'دیجی‌پی', 'website.ePayment.gateway.digipay': 'دیجی‌پی',
'website.ePayment.gateway.digipayDesc': 'پرداخت دیجی‌پی — به‌زودی.', 'website.ePayment.gateway.digipayDesc': 'پرداخت دیجی‌پی — به‌زودی.',
'website.ePayment.gateway.zarinpal': 'زرین‌پال', 'website.ePayment.gateway.zarinpal': 'زرین‌پال',
'website.ePayment.gateway.zarinpalDesc': 'درگاه زرین‌پال — به‌زودی.', 'website.ePayment.gateway.zarinpalDesc':
'کد درگاه (Merchant ID) را از پنل زرین‌پال وارد کنید. اگر ملت و زرین‌پال هر دو فعال باشند، مشتری در مرحله پرداخت یکی را انتخاب می‌کند.',
'website.ePayment.comingSoon': 'به‌زودی', 'website.ePayment.comingSoon': 'به‌زودی',
'website.ePayment.terminalId': 'شناسه ترمینال', 'website.ePayment.terminalId': 'شناسه ترمینال',
'website.ePayment.username': 'نام کاربری', 'website.ePayment.username': 'نام کاربری',
'website.ePayment.password': 'رمز عبور', 'website.ePayment.password': 'رمز عبور',
'website.ePayment.merchantId': 'کد درگاه (Merchant ID)',
'website.ePayment.merchantIdPlaceholder': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'website.ePayment.sandbox': 'حالت آزمایشی (Sandbox)',
'website.ePayment.sandboxDesc':
'برای تست از محیط سندباکس زرین‌پال استفاده می‌شود. برای پرداخت واقعی خاموش کنید.',
'website.ePayment.passwordSet': 'ذخیره‌شده', 'website.ePayment.passwordSet': 'ذخیره‌شده',
'website.ePayment.passwordPlaceholder': 'رمز عبور درگاه را وارد کنید', 'website.ePayment.passwordPlaceholder': 'رمز عبور درگاه را وارد کنید',
'website.ePayment.passwordPlaceholderSet': 'برای حفظ رمز فعلی خالی بگذارید', 'website.ePayment.passwordPlaceholderSet': 'برای حفظ رمز فعلی خالی بگذارید',
@@ -3318,6 +3333,8 @@ const fa: Record<MessageKey, string> = {
'با فعال بودن ملت، شناسه ترمینال و نام کاربری الزامی است.', 'با فعال بودن ملت، شناسه ترمینال و نام کاربری الزامی است.',
'website.ePayment.error.passwordRequired': 'website.ePayment.error.passwordRequired':
'برای اولین بار فعال‌سازی ملت، رمز عبور الزامی است.', 'برای اولین بار فعال‌سازی ملت، رمز عبور الزامی است.',
'website.ePayment.error.zarinpalMerchant':
'با فعال بودن زرین‌پال، کد درگاه (Merchant ID) الزامی است.',
'title.signIn': 'ورود', 'title.signIn': 'ورود',
'title.home': 'خانه', 'title.home': 'خانه',
@@ -3332,7 +3349,7 @@ const fa: Record<MessageKey, string> = {
'title.brands': 'برندها', 'title.brands': 'برندها',
'title.settings': 'تنظیمات', 'title.settings': 'تنظیمات',
'title.store': 'فروشگاه', 'title.store': 'فروشگاه',
'title.storeItems': 'اقلام فروشگاه', 'title.storeItems': 'محصولات فروشگاه',
'title.orders': 'سفارش‌های من', 'title.orders': 'سفارش‌های من',
'title.shoppingCards': 'کارت‌های خرید', 'title.shoppingCards': 'کارت‌های خرید',
'title.customers': 'کاربران', 'title.customers': 'کاربران',
@@ -14,6 +14,82 @@
border-radius: var(--radius); border-radius: var(--radius);
} }
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 56px 28px;
text-align: center;
background: var(--glass-bg);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border: 1px solid var(--glass-border);
border-radius: var(--radius);
box-shadow: var(--glass-shadow);
}
.emptyIcon {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
border-radius: 16px;
color: var(--primary);
background: linear-gradient(
145deg,
rgba(255, 255, 255, 0.72) 0%,
rgba(var(--primary-rgb) / 0.12) 100%
);
border: 1px solid rgba(255, 255, 255, 0.45);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
.emptyTitle {
margin: 0;
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.4;
}
.emptyHint {
margin: 0 0 8px;
max-width: 360px;
font-size: 13px;
line-height: 1.65;
color: var(--text-secondary);
}
.emptyCta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 42px;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
color: #fff;
text-align: center;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: 999px;
box-shadow: 0 8px 20px rgba(var(--primary-rgb) / 0.32);
transition: transform 0.2s, box-shadow 0.2s;
}
button.emptyCta {
text-align: center;
}
.emptyCta:hover {
transform: translateY(-1px);
box-shadow: 0 12px 26px rgba(var(--primary-rgb) / 0.4);
}
.error { .error {
margin-bottom: 16px; margin-bottom: 16px;
padding: 12px 14px; padding: 12px 14px;
+17 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { Plus, RotateCcw, Search } from 'lucide-react' import { Package, Plus, RotateCcw, Search } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs' import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle' import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
@@ -283,7 +283,7 @@ function StoreItemsPageContent() {
<div className={pageStyles.pageHeader}> <div className={pageStyles.pageHeader}>
<div> <div>
<PageTitle en="STORE ITEMS">{t('title.storeItems')}</PageTitle> <PageTitle en="STORE PRODUCTS">{t('title.storeItems')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('storeItems.subtitle')}</p> <p className={pageStyles.pageSubtitle}>{t('storeItems.subtitle')}</p>
</div> </div>
</div> </div>
@@ -370,7 +370,21 @@ function StoreItemsPageContent() {
{isLoading ? ( {isLoading ? (
<p className={styles.empty}>{t('storeItems.loading')}</p> <p className={styles.empty}>{t('storeItems.loading')}</p>
) : listings.length === 0 ? ( ) : listings.length === 0 ? (
<p className={styles.empty}>{t('storeItems.empty')}</p> <div className={styles.emptyState}>
<div className={styles.emptyIcon} aria-hidden="true">
<Package size={28} strokeWidth={1.75} />
</div>
<h3 className={styles.emptyTitle}>{t('storeItems.empty')}</h3>
<p className={styles.emptyHint}>{t('storeItems.emptyHint')}</p>
<button
type="button"
className={styles.emptyCta}
onClick={() => setCreateOpen(true)}
>
<Plus size={18} strokeWidth={2.25} />
{t('storeItems.add')}
</button>
</div>
) : filteredListings.length === 0 ? ( ) : filteredListings.length === 0 ? (
<p className={styles.empty}>{t('storeItems.emptyFiltered')}</p> <p className={styles.empty}>{t('storeItems.emptyFiltered')}</p>
) : ( ) : (