Add store items pagination and fix modal backdrop blur across dashboards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 18:12:17 +03:30
co-authored by Cursor
parent 4d480799b0
commit e6670ed06f
18 changed files with 290 additions and 133 deletions
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.25); isolation: isolate;
backdrop-filter: blur(6px); background: transparent;
-webkit-backdrop-filter: blur(6px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,6 +10,19 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
width: 100%; width: 100%;
max-width: 520px; max-width: 520px;
@@ -135,14 +147,6 @@
box-shadow: 0 6px 16px rgba(var(--primary-rgb) / 0.35); box-shadow: 0 6px 16px rgba(var(--primary-rgb) / 0.35);
} }
.overlayIn {
animation: overlayFadeIn 0.22s ease forwards;
}
.overlayOut {
animation: overlayFadeOut 0.22s ease forwards;
}
.modalIn { .modalIn {
animation: modalFadeIn 0.25s ease forwards; animation: modalFadeIn 0.25s ease forwards;
} }
@@ -150,17 +154,6 @@
.modalOut { .modalOut {
animation: modalFadeOut 0.22s ease forwards; animation: modalFadeOut 0.22s ease forwards;
} }
@keyframes overlayFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes overlayFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { from {
opacity: 0; opacity: 0;
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.25); isolation: isolate;
backdrop-filter: blur(6px); background: transparent;
-webkit-backdrop-filter: blur(6px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,6 +10,19 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
@@ -147,14 +159,6 @@
box-shadow: 0 6px 16px rgba(var(--primary-rgb) / 0.35); box-shadow: 0 6px 16px rgba(var(--primary-rgb) / 0.35);
} }
.overlayIn {
animation: overlayFadeIn 0.22s ease forwards;
}
.overlayOut {
animation: overlayFadeOut 0.22s ease forwards;
}
.modalIn { .modalIn {
animation: modalFadeIn 0.25s ease forwards; animation: modalFadeIn 0.25s ease forwards;
} }
@@ -162,17 +166,6 @@
.modalOut { .modalOut {
animation: modalFadeOut 0.22s ease forwards; animation: modalFadeOut 0.22s ease forwards;
} }
@keyframes overlayFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes overlayFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { from {
opacity: 0; opacity: 0;
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.28); isolation: isolate;
backdrop-filter: blur(6px); background: transparent;
-webkit-backdrop-filter: blur(6px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,6 +10,19 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -174,13 +186,8 @@
cursor: not-allowed; cursor: not-allowed;
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
.modalIn { animation: modalFadeIn 0.25s ease forwards; } .modalIn { animation: modalFadeIn 0.25s ease forwards; }
.modalOut { animation: modalFadeOut 0.22s ease forwards; } .modalOut { animation: modalFadeOut 0.22s ease forwards; }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(127, 29, 29, 0.2); isolation: isolate;
backdrop-filter: blur(6px); background: transparent;
-webkit-backdrop-filter: blur(6px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,6 +10,19 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: rgba(127, 29, 29, 0.28);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -105,13 +117,8 @@
color: #ef4444; color: #ef4444;
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
.modalIn { animation: modalFadeIn 0.25s ease forwards; } .modalIn { animation: modalFadeIn 0.25s ease forwards; }
.modalOut { animation: modalFadeOut 0.22s ease forwards; } .modalOut { animation: modalFadeOut 0.22s ease forwards; }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
@@ -1,6 +1,7 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
isolation: isolate;
z-index: 300; z-index: 300;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -8,18 +9,20 @@
justify-content: center; justify-content: center;
gap: 16px; gap: 16px;
padding: 24px; padding: 24px;
background: transparent;
}
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: rgba(15, 23, 42, 0.82); background: rgba(15, 23, 42, 0.82);
backdrop-filter: blur(8px); backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(var(--modal-overlay-blur));
} }
.overlayIn {
animation: fadeIn 0.2s ease forwards;
}
.overlayOut {
animation: fadeOut 0.2s ease forwards;
}
.closeBtn { .closeBtn {
position: absolute; position: absolute;
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.25); isolation: isolate;
backdrop-filter: blur(6px); background: transparent;
-webkit-backdrop-filter: blur(6px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,8 +10,18 @@
padding: 20px; padding: 20px;
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; } /* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; } .overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
width: 100%; width: 100%;
@@ -206,9 +215,6 @@
margin-top: 0; margin-top: 0;
padding-top: 0; padding-top: 0;
} }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.35); isolation: isolate;
backdrop-filter: blur(10px); background: transparent;
-webkit-backdrop-filter: blur(10px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,13 +10,23 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.overlayNested { .overlayNested {
z-index: 1010; z-index: 1010;
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
.modal { .modal {
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
@@ -420,9 +429,6 @@
color: #ef4444; color: #ef4444;
margin-bottom: 12px; margin-bottom: 12px;
} }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
+6 -2
View File
@@ -393,6 +393,8 @@ const en = {
'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': 'No store items yet. Use the + button to add products from your catalog.',
'storeItems.emptyFiltered': 'No store items match your filters.', 'storeItems.emptyFiltered': 'No store items match your filters.',
'storeItems.pagination':
'Page {page} / {totalPages} · {perPage} per page · {total} total',
'storeItems.errorLoad': 'Unable to load store items.', 'storeItems.errorLoad': 'Unable to load store items.',
'storeItems.errorRemove': 'Unable to remove store item.', 'storeItems.errorRemove': 'Unable to remove store item.',
'storeItems.add': 'Add store items', 'storeItems.add': 'Add store items',
@@ -1490,6 +1492,8 @@ const fa: Record<MessageKey, string> = {
'storeItems.loading': 'در حال بارگذاری اقلام فروشگاه...', 'storeItems.loading': 'در حال بارگذاری اقلام فروشگاه...',
'storeItems.empty': 'هنوز قلمی در فروشگاه نیست. با دکمه + از کاتالوگ اضافه کنید.', 'storeItems.empty': 'هنوز قلمی در فروشگاه نیست. با دکمه + از کاتالوگ اضافه کنید.',
'storeItems.emptyFiltered': 'هیچ قلمی با فیلترهای شما مطابقت ندارد.', 'storeItems.emptyFiltered': 'هیچ قلمی با فیلترهای شما مطابقت ندارد.',
'storeItems.pagination':
'صفحه {page} / {totalPages} · {perPage} در هر صفحه · {total} مجموع',
'storeItems.errorLoad': 'بارگذاری اقلام فروشگاه ممکن نشد.', 'storeItems.errorLoad': 'بارگذاری اقلام فروشگاه ممکن نشد.',
'storeItems.errorRemove': 'حذف قلم از فروشگاه ممکن نشد.', 'storeItems.errorRemove': 'حذف قلم از فروشگاه ممکن نشد.',
'storeItems.add': 'افزودن اقلام فروشگاه', 'storeItems.add': 'افزودن اقلام فروشگاه',
@@ -1504,7 +1508,7 @@ const fa: Record<MessageKey, string> = {
'storeItems.card.festival': 'جشنواره', 'storeItems.card.festival': 'جشنواره',
'storeItems.card.variantOne': '۱ تنوع', 'storeItems.card.variantOne': '۱ تنوع',
'storeItems.card.variantMany': '{count} تنوع', 'storeItems.card.variantMany': '{count} تنوع',
'storeItems.card.edit': 'ویرایش اقلام فروشگاه', 'storeItems.card.edit': 'ویرایش محصول فروشگاه',
'storeItems.card.discount': 'تنظیم تخفیف', 'storeItems.card.discount': 'تنظیم تخفیف',
'storeItems.card.festivalPoints': 'امتیاز جشنواره', 'storeItems.card.festivalPoints': 'امتیاز جشنواره',
'storeItems.card.remove': 'حذف از فروشگاه', 'storeItems.card.remove': 'حذف از فروشگاه',
@@ -1543,7 +1547,7 @@ const fa: Record<MessageKey, string> = {
'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': 'ذخیره اقلام فروشگاه ممکن نشد.',
+2
View File
@@ -24,6 +24,8 @@
--glass-border: rgba(255, 255, 255, 0.7); --glass-border: rgba(255, 255, 255, 0.7);
--glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.08); --glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.08);
--blur-glass: 28px; --blur-glass: 28px;
--modal-overlay-bg: rgba(15, 23, 42, 0.4);
--modal-overlay-blur: 12px;
--text-primary: #1e293b; --text-primary: #1e293b;
--text-secondary: #64748b; --text-secondary: #64748b;
--text-muted: #94a3b8; --text-muted: #94a3b8;
@@ -24,6 +24,39 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.pagination {
margin-top: 20px;
padding: 0 4px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 38px;
color: var(--text-secondary);
font-weight: 500;
font-size: 12px;
}
.paginationMeta {
grid-column: 1;
justify-self: start;
display: inline-flex;
align-items: center;
height: 38px;
margin: 0;
padding: 0;
line-height: 38px;
font-family: var(--font-ui);
transform: translateY(2px);
}
.pagination > nav {
grid-column: 2;
justify-self: center;
height: 38px;
margin: 0;
padding: 0;
}
.fabDock { .fabDock {
position: fixed; position: fixed;
inset-inline-end: 32px; inset-inline-end: 32px;
+80 -16
View File
@@ -1,9 +1,11 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Plus, RotateCcw, Search } from 'lucide-react' import { Plus, RotateCcw, Search } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs' import { Breadcrumbs } from '../components/Breadcrumbs'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { CreateStoreItemsModal } from '../components/CreateStoreItemsModal' import { CreateStoreItemsModal } from '../components/CreateStoreItemsModal'
import { EditStoreItemsModal } from '../components/EditStoreItemsModal' import { EditStoreItemsModal } from '../components/EditStoreItemsModal'
import { Pagination } from '../components/Pagination'
import { PickStoreVariantModal } from '../components/PickStoreVariantModal' import { PickStoreVariantModal } from '../components/PickStoreVariantModal'
import { ShoppingCartModal } from '../components/ShoppingCartModal' import { ShoppingCartModal } from '../components/ShoppingCartModal'
import { StoreItemCard } from '../components/StoreItemCard' import { StoreItemCard } from '../components/StoreItemCard'
@@ -15,7 +17,8 @@ import { useToast } from '../context/ToastContext'
import { ApiError } from '../lib/api' import { ApiError } from '../lib/api'
import { import {
deleteStoreItemsByProduct, deleteStoreItemsByProduct,
listStoreItems, listAllStoreItems,
STORE_ITEMS_PER_PAGE,
type StoreItem, type StoreItem,
} from '../services/storeItemService' } from '../services/storeItemService'
import type { ShoppingCard } from '../services/shoppingCardService' import type { ShoppingCard } from '../services/shoppingCardService'
@@ -37,6 +40,11 @@ import filterStyles from '../components/ListFiltersPanel.module.css'
import pageStyles from '../components/PageContent.module.css' import pageStyles from '../components/PageContent.module.css'
import styles from './StoreItemsPage.module.css' import styles from './StoreItemsPage.module.css'
function readPage(searchParams: URLSearchParams) {
const pageRaw = Number(searchParams.get('page'))
return Number.isFinite(pageRaw) && pageRaw >= 1 ? Math.floor(pageRaw) : 1
}
export function StoreItemsPage() { export function StoreItemsPage() {
return <StoreItemsPageContent /> return <StoreItemsPageContent />
} }
@@ -44,6 +52,8 @@ export function StoreItemsPage() {
function StoreItemsPageContent() { function StoreItemsPageContent() {
const t = useT() const t = useT()
const { locale } = useLocale() const { locale } = useLocale()
const [searchParams, setSearchParams] = useSearchParams()
const currentPage = readPage(searchParams)
const { itemCount, hasItems, addVariant, loadShoppingCard } = useDraftCart() const { itemCount, hasItems, addVariant, loadShoppingCard } = useDraftCart()
const { showToast } = useToast() const { showToast } = useToast()
const [items, setItems] = useState<StoreItem[]>([]) const [items, setItems] = useState<StoreItem[]>([])
@@ -97,6 +107,20 @@ function StoreItemsPageContent() {
[listings, appliedFilters], [listings, appliedFilters],
) )
const totalListings = filteredListings.length
const totalPages = Math.max(1, Math.ceil(totalListings / STORE_ITEMS_PER_PAGE))
const safePage = Math.min(currentPage, totalPages)
const pagedListings = useMemo(() => {
const start = (safePage - 1) * STORE_ITEMS_PER_PAGE
return filteredListings.slice(start, start + STORE_ITEMS_PER_PAGE)
}, [filteredListings, safePage])
useEffect(() => {
if (!isLoading && currentPage > totalPages) {
writePage(totalPages)
}
}, [isLoading, currentPage, totalPages])
const productItems = useMemo(() => { const productItems = useMemo(() => {
const target = editTarget ?? discountTarget ?? festivalTarget const target = editTarget ?? discountTarget ?? festivalTarget
if (!target) return [] if (!target) return []
@@ -108,6 +132,13 @@ function StoreItemsPageContent() {
[listings], [listings],
) )
function writePage(page: number) {
const next = new URLSearchParams(searchParams)
if (page > 1) next.set('page', String(page))
else next.delete('page')
setSearchParams(next, { replace: true })
}
function applyFilters() { function applyFilters() {
setAppliedFilters({ setAppliedFilters({
name: draftName.trim(), name: draftName.trim(),
@@ -115,6 +146,7 @@ function StoreItemsPageContent() {
maxPrice: parseIrtInput(draftMaxPrice), maxPrice: parseIrtInput(draftMaxPrice),
onlyDiscounted: draftOnlyDiscounted, onlyDiscounted: draftOnlyDiscounted,
}) })
writePage(1)
} }
function clearFilters() { function clearFilters() {
@@ -123,6 +155,12 @@ function StoreItemsPageContent() {
setDraftMaxPrice('') setDraftMaxPrice('')
setDraftOnlyDiscounted(false) setDraftOnlyDiscounted(false)
setAppliedFilters(EMPTY_STORE_LISTING_FILTERS) setAppliedFilters(EMPTY_STORE_LISTING_FILTERS)
writePage(1)
}
function handlePageChange(page: number) {
writePage(page)
window.scrollTo({ top: 0, behavior: 'smooth' })
} }
useEffect(() => { useEffect(() => {
@@ -136,8 +174,8 @@ function StoreItemsPageContent() {
setError('') setError('')
try { try {
const data = await listStoreItems(1, 50, signal) const data = await listAllStoreItems(signal)
setItems(data.items) setItems(data)
} catch (err) { } catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) { if (err instanceof ApiError) {
@@ -215,6 +253,12 @@ function StoreItemsPageContent() {
await deleteStoreItemsByProduct(deleteTarget.productId) await deleteStoreItemsByProduct(deleteTarget.productId)
setItems((prev) => prev.filter((item) => item.productId !== deleteTarget.productId)) setItems((prev) => prev.filter((item) => item.productId !== deleteTarget.productId))
setDeleteTarget(null) setDeleteTarget(null)
const nextTotal = Math.max(0, totalListings - 1)
const nextTotalPages = Math.max(1, Math.ceil(nextTotal / STORE_ITEMS_PER_PAGE))
if (safePage > nextTotalPages) {
writePage(nextTotalPages)
}
} catch (err) { } catch (err) {
if (err instanceof ApiError) { if (err instanceof ApiError) {
setError(err.message) setError(err.message)
@@ -329,20 +373,40 @@ function StoreItemsPageContent() {
) : filteredListings.length === 0 ? ( ) : filteredListings.length === 0 ? (
<p className={styles.empty}>{t('storeItems.emptyFiltered')}</p> <p className={styles.empty}>{t('storeItems.emptyFiltered')}</p>
) : ( ) : (
<div className={styles.grid}> <>
{filteredListings.map((listing) => ( <div className={styles.grid}>
<StoreItemCard {pagedListings.map((listing) => (
key={listing.productId} <StoreItemCard
listing={listing} key={listing.productId}
onOpen={openEdit} listing={listing}
onEdit={openEdit} onOpen={openEdit}
onDiscount={setDiscountTarget} onEdit={openEdit}
onFestival={setFestivalTarget} onDiscount={setDiscountTarget}
onRemove={setDeleteTarget} onFestival={setFestivalTarget}
onAddToCart={handleAddToCart} onRemove={setDeleteTarget}
onAddToCart={handleAddToCart}
/>
))}
</div>
<div className={styles.pagination}>
<div className={styles.paginationMeta}>
{t('storeItems.pagination', {
page: safePage,
totalPages,
perPage: STORE_ITEMS_PER_PAGE,
total: totalListings,
})}
</div>
<Pagination
currentPage={safePage}
totalPages={totalPages}
onPageChange={handlePageChange}
disabled={isLoading}
variant="inline"
/> />
))} </div>
</div> </>
)} )}
<div className={styles.fabDock}> <div className={styles.fabDock}>
@@ -1,6 +1,9 @@
import { apiRequest } from '../lib/api' import { apiRequest } from '../lib/api'
import { getActiveBusinessId } from '../lib/businessContext' import { getActiveBusinessId } from '../lib/businessContext'
/** Product cards per page on My Store Items (grid). */
export const STORE_ITEMS_PER_PAGE = 24
export interface StoreItemSelection { export interface StoreItemSelection {
variationId: string variationId: string
variationName: string variationName: string
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.25); isolation: isolate;
backdrop-filter: blur(6px); background: transparent;
-webkit-backdrop-filter: blur(6px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,13 +10,23 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.overlayNested { .overlayNested {
z-index: 210; z-index: 210;
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
.modal { .modal {
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
@@ -412,9 +421,6 @@
color: #ef4444; color: #ef4444;
margin-bottom: 12px; margin-bottom: 12px;
} }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
@@ -1,16 +1,28 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(127, 29, 29, 0.28); isolation: isolate;
backdrop-filter: blur(10px); background: transparent;
-webkit-backdrop-filter: blur(10px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 300; z-index: 200;
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: rgba(127, 29, 29, 0.28);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -105,13 +117,8 @@
color: #ef4444; color: #ef4444;
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
.modalIn { animation: modalFadeIn 0.25s ease forwards; } .modalIn { animation: modalFadeIn 0.25s ease forwards; }
.modalOut { animation: modalFadeOut 0.22s ease forwards; } .modalOut { animation: modalFadeOut 0.22s ease forwards; }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.32); isolation: isolate;
backdrop-filter: blur(10px); background: transparent;
-webkit-backdrop-filter: blur(10px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,6 +10,19 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
width: 100%; width: 100%;
max-width: 520px; max-width: 520px;
+2
View File
@@ -29,6 +29,8 @@
--radius: 16px; --radius: 16px;
--radius-sm: 12px; --radius-sm: 12px;
--blur-glass: 28px; --blur-glass: 28px;
--modal-overlay-bg: rgba(15, 23, 42, 0.4);
--modal-overlay-blur: 12px;
--select-arrow-size: 16px; --select-arrow-size: 16px;
--select-arrow-offset: 12px; --select-arrow-offset: 12px;
--select-padding-end: 2.5rem; --select-padding-end: 2.5rem;
@@ -24,6 +24,8 @@
--glass-border: rgba(255, 255, 255, 0.7); --glass-border: rgba(255, 255, 255, 0.7);
--glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.08); --glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.08);
--blur-glass: 28px; --blur-glass: 28px;
--modal-overlay-bg: rgba(15, 23, 42, 0.4);
--modal-overlay-blur: 12px;
--text-primary: #1e293b; --text-primary: #1e293b;
--text-secondary: #64748b; --text-secondary: #64748b;
--text-muted: #94a3b8; --text-muted: #94a3b8;
@@ -1,9 +1,8 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.25); isolation: isolate;
backdrop-filter: blur(8px); background: transparent;
-webkit-backdrop-filter: blur(8px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,6 +10,19 @@
padding: 20px; padding: 20px;
} }
/* Tint + blur on ::before so opacity animations on .overlay cannot disable backdrop-filter. */
.overlay::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--modal-overlay-bg);
backdrop-filter: blur(var(--modal-overlay-blur));
-webkit-backdrop-filter: blur(var(--modal-overlay-blur));
}
.modal { .modal {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -245,13 +257,8 @@
color: var(--primary); color: var(--primary);
} }
.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
.modalIn { animation: modalFadeIn 0.25s ease forwards; } .modalIn { animation: modalFadeIn 0.25s ease forwards; }
.modalOut { animation: modalFadeOut 0.22s ease forwards; } .modalOut { animation: modalFadeOut 0.22s ease forwards; }
@keyframes overlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { opacity: 0; transform: translateY(12px) scale(0.98); } from { opacity: 0; transform: translateY(12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }