+
+
+ Display
+
onVariantChange(index, "displayImageUrl", value)}
onPreview={onPreview}
/>
-
+
onVariantRemove(index)}
+ aria-label={`Remove ${variant.name || 'variant option'}`}
>
- X
+
@@ -244,6 +270,8 @@ function ProductListItem({ product, isActive, onSelect, collectionName }) {
}
export function ProductWorkspace() {
+ const { productId } = useParams()
+ const navigate = useNavigate()
const [products, setProducts] = useState([])
const [collections, setCollections] = useState([])
const [collectionFilter, setCollectionFilter] = useState('all')
@@ -251,6 +279,7 @@ export function ProductWorkspace() {
const [searchTerm, setSearchTerm] = useState('')
const [selectedId, setSelectedId] = useState(null)
const [draft, setDraft] = useState(null)
+ const [baselineDraft, setBaselineDraft] = useState(null)
const [isCreating, setIsCreating] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
@@ -259,6 +288,11 @@ export function ProductWorkspace() {
const [variants1Enabled, setVariants1Enabled] = useState(false)
const [variants2Enabled, setVariants2Enabled] = useState(false)
const [storeSettings, setStoreSettings] = useState(null)
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [pendingSelection, setPendingSelection] = useState(null)
+ const [unsavedDialogOpen, setUnsavedDialogOpen] = useState(false)
+
+ const isDirty = draftFingerprint(draft) !== draftFingerprint(baselineDraft)
const collectionLookup = useMemo(() => {
const map = new Map()
@@ -308,9 +342,12 @@ export function ProductWorkspace() {
const productData = await productsRes.json()
setProducts(productData)
- if (!isCreating) {
+ if (!isCreating && productId !== 'new') {
if (productData.length > 0) {
setSelectedId((prev) => {
+ if (productId && productData.some((product) => product.id === productId)) {
+ return productId
+ }
if (prev && productData.some((product) => product.id === prev)) {
return prev
}
@@ -319,6 +356,7 @@ export function ProductWorkspace() {
} else {
setSelectedId(null)
setDraft(null)
+ setBaselineDraft(null)
}
}
@@ -361,6 +399,28 @@ export function ProductWorkspace() {
load()
}, [])
+ useEffect(() => {
+ if (productId === 'new') {
+ if (!isCreating) {
+ const blank = createEmptyProduct()
+ setDraft(blank)
+ setBaselineDraft(blank)
+ setIsCreating(true)
+ setSelectedId(null)
+ setVariants1Enabled(false)
+ setVariants2Enabled(false)
+ setStatus(null)
+ }
+ return
+ }
+
+ if (productId && products.some((product) => product.id === productId)) {
+ setIsCreating(false)
+ setSelectedId(productId)
+ setStatus(null)
+ }
+ }, [productId, products, isCreating])
+
useEffect(() => {
if (isCreating) return
if (filteredProducts.length === 0) {
@@ -369,24 +429,29 @@ export function ProductWorkspace() {
}
if (draft) {
setDraft(null)
+ setBaselineDraft(null)
}
return
}
if (!selectedId || !filteredProducts.some((product) => product.id === selectedId)) {
- setSelectedId(filteredProducts[0].id)
+ const nextId = filteredProducts[0].id
+ setSelectedId(nextId)
+ if (!productId) navigate(`/admin/products/${nextId}`, { replace: true })
}
- }, [filteredProducts, isCreating, selectedId, draft])
+ }, [filteredProducts, isCreating, selectedId, draft, productId, navigate])
useEffect(() => {
if (isCreating) return
if (!selectedId) {
setDraft(null)
+ setBaselineDraft(null)
return
}
const selectedProduct = products.find((product) => product.id === selectedId)
if (selectedProduct) {
const nextDraft = mapProductToDraft(selectedProduct)
setDraft(nextDraft)
+ setBaselineDraft(nextDraft)
setVariants1Enabled(hasVariantGroup(nextDraft.variants, nextDraft.variantStyle))
setVariants2Enabled(hasVariantGroup(nextDraft.variants2, nextDraft.variantStyle2))
}
@@ -411,19 +476,55 @@ export function ProductWorkspace() {
}
const startCreate = () => {
+ if (isDirty) {
+ setPendingSelection('new')
+ setUnsavedDialogOpen(true)
+ return
+ }
const blank = createEmptyProduct()
setDraft(blank)
+ setBaselineDraft(blank)
setIsCreating(true)
setSelectedId(null)
setVariants1Enabled(false)
setVariants2Enabled(false)
setStatus(null)
+ navigate('/admin/products/new')
}
const handleSelectProduct = (productId) => {
+ if (isDirty) {
+ setPendingSelection(productId)
+ setUnsavedDialogOpen(true)
+ return
+ }
setIsCreating(false)
setSelectedId(productId)
setStatus(null)
+ navigate(`/admin/products/${productId}`)
+ }
+
+ const confirmPendingSelection = () => {
+ const next = pendingSelection
+ setPendingSelection(null)
+ setUnsavedDialogOpen(false)
+ if (next === 'new') {
+ const blank = createEmptyProduct()
+ setDraft(blank)
+ setBaselineDraft(blank)
+ setIsCreating(true)
+ setSelectedId(null)
+ setVariants1Enabled(false)
+ setVariants2Enabled(false)
+ navigate('/admin/products/new')
+ return
+ }
+ if (next) {
+ setIsCreating(false)
+ setSelectedId(next)
+ setStatus(null)
+ navigate(`/admin/products/${next}`)
+ }
}
const handleDraftChange = (key, value) => {
@@ -520,19 +621,24 @@ export function ProductWorkspace() {
setIsCreating(false)
if (filteredProducts.length > 0) {
setSelectedId(filteredProducts[0].id)
+ navigate(`/admin/products/${filteredProducts[0].id}`)
} else {
setDraft(null)
+ setBaselineDraft(null)
+ navigate('/admin/products')
}
return
}
if (!selectedId) {
setDraft(null)
+ setBaselineDraft(null)
return
}
const original = products.find((product) => product.id === selectedId)
if (original) {
const nextDraft = mapProductToDraft(original)
setDraft(nextDraft)
+ setBaselineDraft(nextDraft)
setVariants1Enabled(hasVariantGroup(nextDraft.variants, nextDraft.variantStyle))
setVariants2Enabled(hasVariantGroup(nextDraft.variants2, nextDraft.variantStyle2))
}
@@ -596,9 +702,11 @@ export function ProductWorkspace() {
setSelectedId(savedProduct.id)
const nextDraft = mapProductToDraft(savedProduct)
setDraft(nextDraft)
+ setBaselineDraft(nextDraft)
setVariants1Enabled(hasVariantGroup(nextDraft.variants, nextDraft.variantStyle))
setVariants2Enabled(hasVariantGroup(nextDraft.variants2, nextDraft.variantStyle2))
setStatus({ type: 'success', message: isNew ? 'Product created' : 'Product updated' })
+ navigate(`/admin/products/${savedProduct.id}`, { replace: true })
// Refresh store settings after creating a product to update limit status
if (isNew) {
@@ -614,8 +722,7 @@ export function ProductWorkspace() {
const handleDelete = async () => {
if (!draft?.id || isCreating) return
- const confirmed = window.confirm('Delete this product? This action cannot be undone.')
- if (!confirmed) return
+ setDeleteDialogOpen(false)
try {
const response = await adminApiRequest(`/api/admin/products/${draft.id}`, {
method: 'DELETE',
@@ -625,9 +732,11 @@ export function ProductWorkspace() {
}
setProducts((prev) => prev.filter((product) => product.id !== draft.id))
setDraft(null)
+ setBaselineDraft(null)
setSelectedId(null)
setIsCreating(false)
setStatus({ type: 'success', message: 'Product deleted' })
+ navigate('/admin/products', { replace: true })
// Refresh store settings after deleting a product to update limit status
await fetchStoreSettings()
@@ -798,16 +907,18 @@ export function ProductWorkspace() {
{draft ? (
-
+
{draft.name || (isCreating ? 'New product draft' : 'Untitled product')}
- {isCreating
- ? 'Draft will be created once you save changes.'
- : 'Changes are saved to the live product.'}
+ {isDirty
+ ? 'Unsaved changes'
+ : isCreating
+ ? 'Draft is ready for edits.'
+ : 'All changes saved.'}
@@ -815,7 +926,7 @@ export function ProductWorkspace() {
setDeleteDialogOpen(true)}
disabled={isSaving}
className="border-[var(--admin-error)] text-[var(--admin-error)] hover:bg-[var(--admin-error-bg)]"
size="sm"
@@ -824,10 +935,10 @@ export function ProductWorkspace() {
Delete
)}
-
+
Discard
-
+
{isSaving ? (
<>
@@ -1078,6 +1189,40 @@ export function ProductWorkspace() {
)}
+
+
+
+
+ Delete product?
+
+ {draft?.name || 'This product'} will be permanently removed from the catalog. This cannot be undone.
+
+
+
+ Cancel
+
+ Delete product
+
+
+
+
+
+
+
+
+ Discard unsaved changes?
+
+ Switching products will discard edits that have not been saved.
+
+
+
+ Keep editing
+
+ Discard changes
+
+
+
+
)
}
diff --git a/src/components/admin/StoreSettingsEditor.jsx b/src/components/admin/StoreSettingsEditor.jsx
index a5a872f..8377421 100644
--- a/src/components/admin/StoreSettingsEditor.jsx
+++ b/src/components/admin/StoreSettingsEditor.jsx
@@ -95,6 +95,7 @@ export function StoreSettingsEditor() {
businessPostalCode: '',
businessCountry: ''
})
+ const [settingsBaseline, setSettingsBaseline] = useState(null)
const [saving, setSaving] = useState(false)
const [modalImage, setModalImage] = useState(null)
const [savedOpen, setSavedOpen] = useState(false)
@@ -114,6 +115,10 @@ export function StoreSettingsEditor() {
const previewTheme = useMemo(() => resolveStorefrontTheme(themeState), [themeState])
const previewStyles = useMemo(() => ({ ...previewTheme.cssVariables }), [previewTheme])
+ const settingsDirty = useMemo(
+ () => JSON.stringify(settings) !== JSON.stringify(settingsBaseline || settings),
+ [settings, settingsBaseline]
+ )
const selectedFontOption = useMemo(
() => FONT_OPTIONS.find((font) => font.id === themeState.typography.fontId) || FONT_OPTIONS[0],
[themeState.typography.fontId]
@@ -155,15 +160,19 @@ export function StoreSettingsEditor() {
const response = await fetch('/api/store-settings')
if (response.ok) {
const data = await response.json()
- setSettings(prev => ({
- ...prev,
- ...data,
- // Ensure about page fields have defaults if missing
- aboutHeroImageUrl: data.aboutHeroImageUrl || '',
- aboutHeroTitle: data.aboutHeroTitle || prev.aboutHeroTitle,
- aboutHeroSubtitle: data.aboutHeroSubtitle || prev.aboutHeroSubtitle,
- aboutContent: data.aboutContent || prev.aboutContent || ''
- }))
+ setSettings(prev => {
+ const nextSettings = {
+ ...prev,
+ ...data,
+ // Ensure about page fields have defaults if missing
+ aboutHeroImageUrl: data.aboutHeroImageUrl || '',
+ aboutHeroTitle: data.aboutHeroTitle || prev.aboutHeroTitle,
+ aboutHeroSubtitle: data.aboutHeroSubtitle || prev.aboutHeroSubtitle,
+ aboutContent: data.aboutContent || prev.aboutContent || ''
+ }
+ setSettingsBaseline(nextSettings)
+ return nextSettings
+ })
}
} catch (error) {
console.error('Error fetching store settings:', error)
@@ -341,6 +350,7 @@ export function StoreSettingsEditor() {
if (response.ok) {
const updatedSettings = await response.json()
setSettings(updatedSettings)
+ setSettingsBaseline(updatedSettings)
setSavedOpen(true)
} else {
const error = await response.json()
@@ -519,7 +529,7 @@ export function StoreSettingsEditor() {
onPreview={(src) => setModalImage(src)}
hideInput
/>
- {driveNotice &&
{driveNotice}
}
+ {driveNotice &&
{driveNotice}
}
Recommended size: 200x50px
)}
@@ -592,7 +602,7 @@ export function StoreSettingsEditor() {
value={settings.aboutContent}
onChange={handleChange}
rows={8}
- className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent text-sm"
+ className="w-full px-3 py-2 border border-[var(--admin-border-primary)] rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--admin-accent)] focus:border-transparent text-sm bg-[var(--admin-bg-elevated)] text-[var(--admin-text-primary)]"
/>