diff --git a/apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css
new file mode 100644
index 0000000..8691260
--- /dev/null
+++ b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.module.css
@@ -0,0 +1,191 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(15, 23, 42, 0.28);
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1200;
+ padding: 20px;
+}
+
+.modal {
+ position: relative;
+ width: 100%;
+ max-width: 440px;
+ padding: 32px 28px 24px;
+ text-align: start;
+ background: rgba(255, 255, 255, 0.94);
+ backdrop-filter: blur(24px);
+ -webkit-backdrop-filter: blur(24px);
+ border: 1px solid rgba(245, 158, 11, 0.28);
+ border-radius: var(--radius);
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.18);
+ font-family: var(--font-ui);
+}
+
+.iconWrap {
+ width: 56px;
+ height: 56px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 16px;
+ background: rgba(245, 158, 11, 0.14);
+ color: #d97706;
+ border-radius: 50%;
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+ text-align: center;
+ font-family: var(--font-ui);
+}
+
+.message {
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--text-secondary);
+ margin-bottom: 12px;
+ text-align: center;
+ font-family: var(--font-ui);
+}
+
+.points {
+ margin: 0 0 18px;
+ padding-inline-start: 1.25rem;
+ font-size: 13px;
+ line-height: 1.65;
+ color: var(--text-secondary);
+ font-family: var(--font-ui);
+}
+
+.points li + li {
+ margin-top: 4px;
+}
+
+.agree {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ margin-bottom: 22px;
+ padding: 12px 14px;
+ font-size: 13px;
+ line-height: 1.45;
+ color: var(--text-primary);
+ font-family: var(--font-ui);
+ background: rgba(148, 163, 184, 0.1);
+ border: 1px solid rgba(148, 163, 184, 0.22);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ text-align: start;
+}
+
+.agreeCheck {
+ width: 16px;
+ height: 16px;
+ margin-top: 2px;
+ flex-shrink: 0;
+ accent-color: var(--primary);
+ cursor: pointer;
+}
+
+.agreeCheck:disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
+.actions {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.cancelBtn {
+ padding: 10px 20px;
+ min-height: var(--field-height);
+ font-size: 14px;
+ font-weight: 500;
+ font-family: var(--font-ui);
+ color: var(--text-secondary);
+ border-radius: var(--radius-sm);
+ transition: background 0.2s;
+}
+
+.cancelBtn:hover:not(:disabled) {
+ background: rgba(148, 163, 184, 0.15);
+}
+
+.cancelBtn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
+.confirmBtn {
+ padding: 10px 22px;
+ min-height: var(--field-height);
+ font-size: 14px;
+ font-weight: 600;
+ font-family: var(--font-ui);
+ color: white;
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 4px 12px rgba(var(--primary-rgb) / 0.3);
+ transition: transform 0.2s, opacity 0.2s;
+}
+
+.confirmBtn:hover:not(:disabled) {
+ transform: translateY(-1px);
+}
+
+.confirmBtn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.closeBtn {
+ position: absolute;
+ top: 14px;
+ inset-inline-end: 14px;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ color: var(--text-muted);
+ transition: background 0.2s, color 0.2s;
+}
+
+.closeBtn:hover:not(:disabled) {
+ background: rgba(245, 158, 11, 0.12);
+ color: #d97706;
+}
+
+.closeBtn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.overlayIn { animation: overlayFadeIn 0.22s ease forwards; }
+.overlayOut { animation: overlayFadeOut 0.22s ease forwards; }
+.modalIn { animation: modalFadeIn 0.25s 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 {
+ from { opacity: 0; transform: translateY(12px) scale(0.98); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+@keyframes modalFadeOut {
+ from { opacity: 1; transform: translateY(0) scale(1); }
+ to { opacity: 0; transform: translateY(8px) scale(0.98); }
+}
diff --git a/apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx
new file mode 100644
index 0000000..8bb2ca1
--- /dev/null
+++ b/apps/business/src/components/ConfirmBulkAddStoreItemsModal.tsx
@@ -0,0 +1,164 @@
+import { useEffect, useState } from 'react'
+import { createPortal } from 'react-dom'
+import { AlertTriangle, PackagePlus, X } from 'lucide-react'
+import { useLocale } from '@meshkee/dashboard-ui'
+import { useT } from '../i18n/useT'
+import styles from './ConfirmBulkAddStoreItemsModal.module.css'
+
+export interface BulkAddResult {
+ created: number
+ skipped: number
+ failed: number
+}
+
+interface ConfirmBulkAddStoreItemsModalProps {
+ open: boolean
+ busy: boolean
+ result: BulkAddResult | null
+ onConfirm: () => void
+ onClose: () => void
+}
+
+const ANIMATION_MS = 220
+
+export function ConfirmBulkAddStoreItemsModal({
+ open,
+ busy,
+ result,
+ onConfirm,
+ onClose,
+}: ConfirmBulkAddStoreItemsModalProps) {
+ const t = useT()
+ const { locale } = useLocale()
+ const isFa = locale === 'fa'
+ const [mounted, setMounted] = useState(open)
+ const [closing, setClosing] = useState(false)
+ const [agreed, setAgreed] = useState(false)
+
+ useEffect(() => {
+ if (open) {
+ setMounted(true)
+ setClosing(false)
+ setAgreed(false)
+ } else if (mounted) {
+ setClosing(true)
+ const timer = setTimeout(() => {
+ setMounted(false)
+ setClosing(false)
+ setAgreed(false)
+ }, ANIMATION_MS)
+ return () => clearTimeout(timer)
+ }
+ }, [open, mounted])
+
+ useEffect(() => {
+ if (!mounted || closing) return
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && !busy) onClose()
+ }
+ document.addEventListener('keydown', onKey)
+ return () => document.removeEventListener('keydown', onKey)
+ }, [mounted, closing, onClose, busy])
+
+ if (!mounted) return null
+
+ const showResult = result !== null
+
+ return createPortal(
+
{
+ if (!busy) onClose()
+ }}
+ >
+
e.stopPropagation()}
+ role="alertdialog"
+ aria-modal="true"
+ aria-labelledby="confirm-bulk-add-title"
+ lang={isFa ? 'fa' : 'en'}
+ dir={isFa ? 'rtl' : 'ltr'}
+ >
+
+
+
+ {showResult
+ ? t('storeItems.create.addAllResultTitle')
+ : t('storeItems.create.addAllConfirmTitle')}
+
+
+ {showResult ? (
+
+ {t('storeItems.create.addAllDone', {
+ created: String(result.created),
+ skipped: String(result.skipped),
+ failed: String(result.failed),
+ })}
+
+ ) : (
+ <>
+
{t('storeItems.create.addAllConfirmIntro')}
+
+ - {t('storeItems.create.addAllConfirmMax')}
+ - {t('storeItems.create.addAllConfirmSkip')}
+ - {t('storeItems.create.addAllConfirmHeavy')}
+
+
+
+ >
+ )}
+
+
+ {showResult ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
,
+ document.body,
+ )
+}
diff --git a/apps/business/src/components/CreateStoreItemsModal.module.css b/apps/business/src/components/CreateStoreItemsModal.module.css
index 7241338..768be58 100644
--- a/apps/business/src/components/CreateStoreItemsModal.module.css
+++ b/apps/business/src/components/CreateStoreItemsModal.module.css
@@ -65,6 +65,14 @@
cursor: pointer;
}
+.rowActions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
.addRowBtn {
display: inline-flex;
align-items: center;
@@ -76,13 +84,82 @@
color: var(--primary);
border-radius: var(--radius-sm);
transition: background 0.2s;
- margin-bottom: 8px;
}
-.addRowBtn:hover {
+.addRowBtn:hover:not(:disabled) {
background: rgba(var(--primary-rgb) / 0.1);
}
+.addRowBtn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
+.footerActions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding-top: 16px;
+ margin-top: 8px;
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+.footerBulkBtn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 14px;
+ min-height: var(--field-height);
+ font-size: 13px;
+ font-weight: 500;
+ font-family: var(--font-ui);
+ color: var(--primary);
+ border-radius: var(--radius-sm);
+ transition: background 0.2s;
+}
+
+.footerBulkBtn:hover:not(:disabled) {
+ background: rgba(var(--primary-rgb) / 0.1);
+}
+
+.footerBulkBtn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
+.footerEnd {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ margin-inline-start: auto;
+}
+
+.unlimitedCell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.unlimitedCheck {
+ width: 16px;
+ height: 16px;
+ accent-color: var(--primary);
+ cursor: pointer;
+}
+
+.unlimitedCheck:disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
+.compactInput:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
.modalWide {
max-width: 920px;
}
diff --git a/apps/business/src/components/CreateStoreItemsModal.tsx b/apps/business/src/components/CreateStoreItemsModal.tsx
index 2b89808..e3886aa 100644
--- a/apps/business/src/components/CreateStoreItemsModal.tsx
+++ b/apps/business/src/components/CreateStoreItemsModal.tsx
@@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
-import { Plus, X } from 'lucide-react'
+import { PackagePlus, Plus, X } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
+import { useToast } from '../context/ToastContext'
import { useT } from '../i18n/useT'
import { ApiError } from '../lib/api'
import { listAllProducts } from '../services/productService'
@@ -16,10 +17,15 @@ import {
import { createId } from '../utils/id'
import { formatIrtInput, parseIrtInput } from '../utils/irtPrice'
import {
+ buildBulkCreateItems,
createEmptyStoreItemRow,
getVariationOptions,
type StoreItemDraftRow,
} from '../utils/storeItemRows'
+import {
+ ConfirmBulkAddStoreItemsModal,
+ type BulkAddResult,
+} from './ConfirmBulkAddStoreItemsModal'
import { ProductSearchSelect } from './ProductSearchSelect'
import { Tooltip } from './Tooltip'
import modalStyles from './VariationsModal.module.css'
@@ -44,6 +50,7 @@ export function CreateStoreItemsModal({
}: CreateStoreItemsModalProps) {
const t = useT()
const { locale } = useLocale()
+ const { showToast } = useToast()
const [mounted, setMounted] = useState(open)
const [closing, setClosing] = useState(false)
const [products, setProducts] = useState<{ id: string; title: string; nameFa: string }[]>([])
@@ -53,8 +60,13 @@ export function CreateStoreItemsModal({
const [isLoadingProducts, setIsLoadingProducts] = useState(false)
const [isLoadingVariations, setIsLoadingVariations] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isBulkCreating, setIsBulkCreating] = useState(false)
+ const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false)
+ const [bulkResult, setBulkResult] = useState(null)
const [error, setError] = useState('')
+ const busy = isSubmitting || isBulkCreating
+
const formVariations = useMemo(
() => variations.filter((variation) => getVariationOptions(variation).length > 0),
[variations],
@@ -62,8 +74,10 @@ export function CreateStoreItemsModal({
const gridTemplate = useMemo(() => {
const variationCols = formVariations.map(() => 'minmax(110px, 1fr)').join(' ')
- const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '32px'].filter((part) => part).join(' ')
- return cols || 'minmax(120px, 1fr) 90px 32px'
+ const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '72px', '32px']
+ .filter((part) => part)
+ .join(' ')
+ return cols || 'minmax(120px, 1fr) 90px 72px 32px'
}, [formVariations])
useEffect(() => {
@@ -79,6 +93,8 @@ export function CreateStoreItemsModal({
setVariations([])
setRows([])
setError('')
+ setBulkConfirmOpen(false)
+ setBulkResult(null)
}, ANIMATION_MS)
return () => clearTimeout(timer)
}
@@ -154,11 +170,11 @@ export function CreateStoreItemsModal({
useEffect(() => {
if (!mounted || closing) return
const onKey = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose()
+ if (e.key === 'Escape' && !busy && !bulkConfirmOpen) onClose()
}
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
- }, [mounted, closing, onClose])
+ }, [mounted, closing, onClose, busy, bulkConfirmOpen])
if (!mounted) return null
@@ -205,8 +221,12 @@ export function CreateStoreItemsModal({
if (!optionId) return []
return [{ variationId: variation.id, optionId }]
}),
- price: parseIrtInput(row.price) ?? undefined,
- stockQuantity: row.stock.trim() ? Number(row.stock) : undefined,
+ price: parseIrtInput(row.price),
+ stockQuantity: row.unlimitedStock
+ ? null
+ : row.stock.trim()
+ ? Number(row.stock)
+ : null,
}))
}
@@ -215,10 +235,12 @@ export function CreateStoreItemsModal({
rows.length > 0 &&
rows.every((row) => {
const price = parseIrtInput(row.price)
+ if (price !== null && price < 0) return false
+ if (row.unlimitedStock) return true
const stock = Number(row.stock)
- return price !== null && price >= 0 && row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0
+ return row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0
}) &&
- !isSubmitting &&
+ !busy &&
!isLoadingVariations
async function handleSubmit(e: React.FormEvent) {
@@ -246,10 +268,91 @@ export function CreateStoreItemsModal({
}
}
+ function openBulkConfirm() {
+ if (busy || isLoadingProducts || products.length === 0) return
+ setBulkResult(null)
+ setBulkConfirmOpen(true)
+ }
+
+ function closeBulkConfirm() {
+ if (isBulkCreating) return
+ const hadResult = bulkResult !== null
+ const createdCount = bulkResult?.created ?? 0
+ setBulkConfirmOpen(false)
+ setBulkResult(null)
+ if (hadResult) {
+ onCreated?.()
+ if (createdCount > 0) onClose()
+ }
+ }
+
+ async function handleAddAllProducts() {
+ if (isBulkCreating || isLoadingProducts) return
+
+ setIsBulkCreating(true)
+ setError('')
+ setBulkResult(null)
+
+ let created = 0
+ let skipped = 0
+ let failed = 0
+ const skip = new Set(existingProductIds)
+
+ try {
+ const catalog =
+ products.length > 0
+ ? products
+ : (await listAllProducts()).map((item) => ({
+ id: item.id,
+ title: item.title,
+ nameFa: item.nameFa,
+ }))
+
+ for (const product of catalog) {
+ if (skip.has(product.id)) {
+ skipped += 1
+ continue
+ }
+
+ try {
+ const data = await getProductVariationValues(product.id)
+ const items = buildBulkCreateItems(data.variations)
+ await batchCreateStoreItems({ productId: product.id, items })
+ created += 1
+ skip.add(product.id)
+ } catch {
+ failed += 1
+ }
+ }
+
+ const result = { created, skipped, failed }
+ setBulkResult(result)
+ showToast(
+ t('storeItems.create.addAllDone', {
+ created: String(created),
+ skipped: String(skipped),
+ failed: String(failed),
+ }),
+ failed > 0 && created === 0 ? 'error' : 'success',
+ )
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setError(err.message)
+ } else {
+ setError(t('storeItems.create.errorCreate'))
+ }
+ setBulkConfirmOpen(false)
+ } finally {
+ setIsBulkCreating(false)
+ }
+ }
+
return createPortal(
{
+ if (!busy && !bulkConfirmOpen) onClose()
+ }}
>
@@ -284,7 +388,7 @@ export function CreateStoreItemsModal({
value={productId}
onChange={handleProductChange}
placeholder={t('storeItems.create.productPlaceholder')}
- disabled={isLoadingProducts || isSubmitting}
+ disabled={isLoadingProducts || busy}
aria-label={t('storeItems.create.productPlaceholder')}
/>
@@ -304,6 +408,7 @@ export function CreateStoreItemsModal({
))}
{t('storeItems.create.price')}
{t('storeItems.create.stock')}
+
{t('storeItems.create.unlimited')}
@@ -324,7 +429,7 @@ export function CreateStoreItemsModal({
onChange={(e) =>
updateRowSelection(row.id, variation.id, e.target.value)
}
- disabled={isSubmitting}
+ disabled={busy}
>
{options.map((option) => (
@@ -341,25 +446,38 @@ export function CreateStoreItemsModal({
className={rowStyles.compactInput}
value={row.price}
onChange={(e) => updateRow(row.id, { price: formatIrtInput(e.target.value) })}
- placeholder="0"
- disabled={isSubmitting}
+ placeholder={t('storeItems.price.contact')}
+ disabled={busy}
/>
updateRow(row.id, { stock: e.target.value })}
- disabled={isSubmitting}
+ disabled={busy || row.unlimitedStock}
+ placeholder={row.unlimitedStock ? '∞' : '0'}
/>
+
+
+ updateRow(row.id, { unlimitedStock: e.target.checked })
+ }
+ disabled={busy}
+ aria-label={t('storeItems.create.unlimited')}
+ />
+
-
+
+
+
>
)}
{error && {error}
}
-
+
-
+
+
+
+
+
+ void handleAddAllProducts()}
+ onClose={closeBulkConfirm}
+ />
,
document.body,
diff --git a/apps/business/src/components/EditStoreItemsModal.tsx b/apps/business/src/components/EditStoreItemsModal.tsx
index e30e982..ba5ed3f 100644
--- a/apps/business/src/components/EditStoreItemsModal.tsx
+++ b/apps/business/src/components/EditStoreItemsModal.tsx
@@ -43,12 +43,15 @@ function itemsToRows(storeItems: StoreItem[]): StoreItemDraftRow[] {
selections[selection.variationId] = selection.optionId
}
+ const unlimited = item.stockQuantity === null
+
return {
id: createId(),
storeItemId: item.id,
selections,
price: item.price !== null ? formatIrtInput(String(item.price)) : '',
- stock: item.stockQuantity !== null ? String(item.stockQuantity) : '1',
+ stock: unlimited ? '1' : String(item.stockQuantity),
+ unlimitedStock: unlimited,
}
})
}
@@ -82,8 +85,10 @@ export function EditStoreItemsModal({
const gridTemplate = useMemo(() => {
const variationCols = formVariations.map(() => 'minmax(110px, 1fr)').join(' ')
- const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '32px'].filter((part) => part).join(' ')
- return cols || 'minmax(120px, 1fr) 90px 32px'
+ const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '72px', '32px']
+ .filter((part) => part)
+ .join(' ')
+ return cols || 'minmax(120px, 1fr) 90px 72px 32px'
}, [formVariations])
useEffect(() => {
@@ -128,11 +133,11 @@ export function EditStoreItemsModal({
useEffect(() => {
if (!mounted || closing) return
const onKey = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose()
+ if (e.key === 'Escape' && !isSubmitting) onClose()
}
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
- }, [mounted, closing, onClose])
+ }, [mounted, closing, onClose, isSubmitting])
if (!mounted || !productId) return null
@@ -174,8 +179,10 @@ export function EditStoreItemsModal({
rows.length > 0 &&
rows.every((row) => {
const price = parseIrtInput(row.price)
+ if (price !== null && price < 0) return false
+ if (row.unlimitedStock) return true
const stock = Number(row.stock)
- return price !== null && price >= 0 && row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0
+ return row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0
}) &&
!isSubmitting &&
!isLoading
@@ -197,8 +204,12 @@ export function EditStoreItemsModal({
if (!optionId) return []
return [{ variationId: variation.id, optionId }]
}),
- price: parseIrtInput(row.price)!,
- stockQuantity: Number(row.stock),
+ price: parseIrtInput(row.price),
+ stockQuantity: row.unlimitedStock
+ ? null
+ : row.stock.trim()
+ ? Number(row.stock)
+ : null,
})),
removedIds,
})
@@ -222,7 +233,9 @@ export function EditStoreItemsModal({
return createPortal(
{
+ if (!isSubmitting) onClose()
+ }}
>
@@ -263,6 +277,7 @@ export function EditStoreItemsModal({
))}
{t('storeItems.create.price')}
{t('storeItems.create.stock')}
+ {t('storeItems.create.unlimited')}
@@ -300,7 +315,7 @@ export function EditStoreItemsModal({
className={rowStyles.compactInput}
value={row.price}
onChange={(e) => updateRow(row.id, { price: formatIrtInput(e.target.value) })}
- placeholder="0"
+ placeholder={t('storeItems.price.contact')}
disabled={isSubmitting}
/>
updateRow(row.id, { stock: e.target.value })}
- disabled={isSubmitting}
+ disabled={isSubmitting || row.unlimitedStock}
+ placeholder={row.unlimitedStock ? '∞' : '0'}
/>
+
+
+ updateRow(row.id, { unlimitedStock: e.target.checked })
+ }
+ disabled={isSubmitting}
+ aria-label={t('storeItems.create.unlimited')}
+ />
+