All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
1900 lines
73 KiB
JavaScript
1900 lines
73 KiB
JavaScript
import React, { useState, useEffect, useContext } from 'react'
|
||
import { useNavigate, Link } from 'react-router-dom'
|
||
import api from '../api'
|
||
import { AuthContext } from '../context/AuthContext'
|
||
import Toast from '../components/Toast'
|
||
import '../styles/global.css'
|
||
|
||
export default function Admin() {
|
||
const navigate = useNavigate()
|
||
const { user, token } = useContext(AuthContext)
|
||
const [toast, setToast] = useState(null)
|
||
const [products, setProducts] = useState([])
|
||
const [categories, setCategories] = useState([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [editingProduct, setEditingProduct] = useState(null)
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
slug: '',
|
||
description: '',
|
||
price: '',
|
||
discount_price: '',
|
||
category_id: '',
|
||
model_id: '',
|
||
gender: 'men',
|
||
brand: '',
|
||
sizes: '',
|
||
stock: '',
|
||
images: '',
|
||
is_featured: false,
|
||
is_on_sale: false,
|
||
override_price: '',
|
||
override_sizes: '',
|
||
})
|
||
const [uploadingImage, setUploadingImage] = useState(false)
|
||
const [uploadedImages, setUploadedImages] = useState([])
|
||
const [models, setModels] = useState([])
|
||
const [activeTab, setActiveTab] = useState('products') // products, categories, brands, models, messages
|
||
const [showCategoryForm, setShowCategoryForm] = useState(false)
|
||
const [editingCategory, setEditingCategory] = useState(null)
|
||
const [categoryFormData, setCategoryFormData] = useState({
|
||
name: '',
|
||
slug: '',
|
||
description: '',
|
||
image: '',
|
||
})
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [filterBrand, setFilterBrand] = useState('')
|
||
const [filterCategory, setFilterCategory] = useState('')
|
||
const [filterModel, setFilterModel] = useState('')
|
||
const [brands, setBrands] = useState([])
|
||
const [allProducts, setAllProducts] = useState([]) // Store all products for filtering
|
||
const [showBrandForm, setShowBrandForm] = useState(false)
|
||
const [editingBrand, setEditingBrand] = useState(null)
|
||
const [brandFormData, setBrandFormData] = useState({ name: '' })
|
||
const [brandsList, setBrandsList] = useState([]) // Separate list for brand management
|
||
const [showModelForm, setShowModelForm] = useState(false)
|
||
const [editingModel, setEditingModel] = useState(null)
|
||
const [modelFormData, setModelFormData] = useState({
|
||
name: '',
|
||
category_id: '',
|
||
brand: '',
|
||
base_price: '',
|
||
sizes: '',
|
||
stock: '',
|
||
description: '',
|
||
})
|
||
|
||
// Contact Messages state
|
||
const [contactMessages, setContactMessages] = useState([])
|
||
const [filteredMessages, setFilteredMessages] = useState([])
|
||
const [unreadCount, setUnreadCount] = useState(0)
|
||
const [messageFilter, setMessageFilter] = useState('all') // all, new, read, replied
|
||
const [selectedMessage, setSelectedMessage] = useState(null)
|
||
const [showMessageModal, setShowMessageModal] = useState(false)
|
||
const [messageNotes, setMessageNotes] = useState('')
|
||
|
||
// Redirect if not admin
|
||
useEffect(() => {
|
||
if (!user?.is_admin) {
|
||
navigate('/')
|
||
}
|
||
}, [user, navigate])
|
||
|
||
useEffect(() => {
|
||
fetchProducts()
|
||
fetchCategories()
|
||
fetchModels()
|
||
fetchBrands()
|
||
fetchContactMessages()
|
||
fetchUnreadCount()
|
||
}, [])
|
||
|
||
const fetchProducts = async () => {
|
||
try {
|
||
const response = await api.get('/products')
|
||
setAllProducts(response.data)
|
||
setProducts(response.data)
|
||
} catch (error) {
|
||
console.error('Error fetching products:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const fetchCategories = async () => {
|
||
try {
|
||
const response = await api.get('/categories')
|
||
setCategories(response.data)
|
||
} catch (error) {
|
||
console.error('Error fetching categories:', error)
|
||
}
|
||
}
|
||
|
||
const fetchModels = async () => {
|
||
try {
|
||
const response = await api.get('/models')
|
||
setModels(response.data)
|
||
} catch (error) {
|
||
console.error('Error fetching models:', error)
|
||
}
|
||
}
|
||
|
||
const fetchBrands = async () => {
|
||
try {
|
||
const response = await api.get('/brands')
|
||
setBrands(response.data.map(b => b.name).sort())
|
||
setBrandsList(response.data)
|
||
} catch (error) {
|
||
console.error('Error fetching brands:', error)
|
||
}
|
||
}
|
||
|
||
const fetchContactMessages = async () => {
|
||
try {
|
||
const response = await api.get('/admin/contact-messages')
|
||
setContactMessages(response.data)
|
||
setFilteredMessages(response.data)
|
||
} catch (error) {
|
||
console.error('Error fetching contact messages:', error)
|
||
}
|
||
}
|
||
|
||
const fetchUnreadCount = async () => {
|
||
try {
|
||
const response = await api.get('/admin/contact-messages/unread-count')
|
||
setUnreadCount(response.data.unread_count)
|
||
} catch (error) {
|
||
console.error('Error fetching unread count:', error)
|
||
}
|
||
}
|
||
|
||
// Filter contact messages based on status
|
||
useEffect(() => {
|
||
if (messageFilter === 'all') {
|
||
setFilteredMessages(contactMessages)
|
||
} else {
|
||
setFilteredMessages(contactMessages.filter(m => m.status === messageFilter))
|
||
}
|
||
}, [messageFilter, contactMessages])
|
||
|
||
// Filter products based on search and filters
|
||
useEffect(() => {
|
||
let filtered = [...allProducts]
|
||
|
||
// Search filter
|
||
if (searchQuery) {
|
||
filtered = filtered.filter(p =>
|
||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
p.brand?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
p.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||
)
|
||
}
|
||
|
||
// Brand filter
|
||
if (filterBrand) {
|
||
filtered = filtered.filter(p => p.brand === filterBrand)
|
||
}
|
||
|
||
// Category filter
|
||
if (filterCategory) {
|
||
filtered = filtered.filter(p => p.category_id === parseInt(filterCategory))
|
||
}
|
||
|
||
// Model filter
|
||
if (filterModel) {
|
||
filtered = filtered.filter(p => p.model_id === parseInt(filterModel))
|
||
}
|
||
|
||
setProducts(filtered)
|
||
}, [searchQuery, filterBrand, filterCategory, filterModel, allProducts])
|
||
|
||
const handleChange = (e) => {
|
||
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value
|
||
setFormData({
|
||
...formData,
|
||
[e.target.name]: value,
|
||
})
|
||
}
|
||
|
||
const handleImageUpload = async (e) => {
|
||
const files = e.target.files
|
||
if (!files || files.length === 0) return
|
||
|
||
setUploadingImage(true)
|
||
const newImages = [...uploadedImages]
|
||
|
||
try {
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i]
|
||
const formDataUpload = new FormData()
|
||
formDataUpload.append('file', file)
|
||
|
||
const response = await api.post('/products/upload-image', formDataUpload, {
|
||
headers: {
|
||
'Content-Type': 'multipart/form-data',
|
||
},
|
||
})
|
||
|
||
// Backend now returns full URLs
|
||
newImages.push(response.data.url)
|
||
}
|
||
|
||
setUploadedImages(newImages)
|
||
// Update form data with new images
|
||
const currentImages = formData.images ? formData.images.split(',').map(s => s.trim()).filter(s => s) : []
|
||
const allImages = [...currentImages, ...newImages.map(url => url)]
|
||
setFormData({ ...formData, images: allImages.join(', ') })
|
||
|
||
setToast({ type: 'success', message: 'Images uploaded successfully!' })
|
||
} catch (error) {
|
||
console.error('Error uploading image:', error)
|
||
setToast({ type: 'error', message: 'Error uploading image: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
} finally {
|
||
setUploadingImage(false)
|
||
}
|
||
}
|
||
|
||
const removeImage = (imageUrl) => {
|
||
const currentImages = formData.images.split(',').map(s => s.trim()).filter(s => s)
|
||
const filtered = currentImages.filter(img => img !== imageUrl)
|
||
setFormData({ ...formData, images: filtered.join(', ') })
|
||
setUploadedImages(uploadedImages.filter(img => img !== imageUrl))
|
||
}
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault()
|
||
|
||
const productData = {
|
||
...formData,
|
||
price: formData.price ? parseFloat(formData.price) : null, // Allow null - inherits from model
|
||
discount_price: formData.discount_price ? parseFloat(formData.discount_price) : null,
|
||
category_id: parseInt(formData.category_id),
|
||
model_id: formData.model_id ? parseInt(formData.model_id) : null,
|
||
stock: formData.stock ? parseInt(formData.stock) : null,
|
||
sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()).filter(s => s) : [],
|
||
images: formData.images.split(',').map(i => i.trim()).filter(i => i),
|
||
override_price: formData.override_price ? parseFloat(formData.override_price) : null,
|
||
override_sizes: formData.override_sizes ? formData.override_sizes.split(',').map(s => s.trim()) : null,
|
||
}
|
||
|
||
try {
|
||
if (editingProduct) {
|
||
await api.put(`/products/${editingProduct.id}`, productData)
|
||
setToast({ type: 'success', message: 'Product updated successfully!' })
|
||
} else {
|
||
await api.post('/products', productData)
|
||
setToast({ type: 'success', message: 'Product created successfully!' })
|
||
}
|
||
|
||
setShowForm(false)
|
||
setEditingProduct(null)
|
||
resetForm()
|
||
fetchProducts()
|
||
} catch (error) {
|
||
console.error('Error saving product:', error)
|
||
setToast({ type: 'error', message: 'Error saving product: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const handleEdit = (product) => {
|
||
console.log('Edit clicked for product:', product)
|
||
try {
|
||
setEditingProduct(product)
|
||
const imageList = Array.isArray(product.images) ? product.images : []
|
||
console.log('Setting form data...')
|
||
setFormData({
|
||
name: product.name || '',
|
||
slug: product.slug || '',
|
||
description: product.description || '',
|
||
price: product.price || '',
|
||
discount_price: product.discount_price || '',
|
||
category_id: product.category_id || '',
|
||
model_id: product.model_id || '',
|
||
gender: product.gender || 'men',
|
||
brand: product.brand || '',
|
||
sizes: Array.isArray(product.sizes) ? product.sizes.join(', ') : '',
|
||
stock: product.stock || '',
|
||
images: imageList.join(', '),
|
||
is_featured: product.is_featured || false,
|
||
is_on_sale: product.is_on_sale || false,
|
||
override_price: product.override_price || '',
|
||
override_sizes: Array.isArray(product.override_sizes) ? product.override_sizes.join(', ') : '',
|
||
})
|
||
setUploadedImages(imageList)
|
||
console.log('Showing form...')
|
||
setShowForm(true)
|
||
console.log('Form should now be visible')
|
||
|
||
// Scroll to top to show the form
|
||
setTimeout(() => {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||
}, 100)
|
||
} catch (error) {
|
||
console.error('Error in handleEdit:', error)
|
||
setToast({ type: 'error', message: 'Error loading product: ' + error.message })
|
||
}
|
||
}
|
||
|
||
const handleDelete = async (id) => {
|
||
if (!confirm('Are you sure you want to delete this product?')) return
|
||
|
||
try {
|
||
await api.delete(`/products/${id}`)
|
||
setToast({ type: 'success', message: 'Product deleted successfully!' })
|
||
fetchProducts()
|
||
} catch (error) {
|
||
console.error('Error deleting product:', error)
|
||
setToast({ type: 'error', message: 'Error deleting product' })
|
||
}
|
||
}
|
||
|
||
const resetForm = () => {
|
||
setFormData({
|
||
name: '',
|
||
slug: '',
|
||
description: '',
|
||
price: '',
|
||
discount_price: '',
|
||
category_id: '',
|
||
model_id: '',
|
||
gender: 'men',
|
||
brand: '',
|
||
sizes: '',
|
||
stock: '',
|
||
images: '',
|
||
is_featured: false,
|
||
is_on_sale: false,
|
||
override_price: '',
|
||
override_sizes: '',
|
||
})
|
||
setUploadedImages([])
|
||
}
|
||
|
||
const handleCancel = () => {
|
||
setShowForm(false)
|
||
setEditingProduct(null)
|
||
resetForm()
|
||
}
|
||
|
||
// Category Management Functions
|
||
const handleCategoryChange = (e) => {
|
||
const { name, value } = e.target
|
||
setCategoryFormData({ ...categoryFormData, [name]: value })
|
||
}
|
||
|
||
const handleCategoryImageUpload = async (e) => {
|
||
const file = e.target.files[0]
|
||
if (!file) return
|
||
|
||
setUploadingImage(true)
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
|
||
try {
|
||
const response = await api.post('/categories/upload-image', formData, {
|
||
headers: { 'Content-Type': 'multipart/form-data' }
|
||
})
|
||
// Backend now returns full URLs
|
||
setCategoryFormData({ ...categoryFormData, image: response.data.url })
|
||
setToast({ type: 'success', message: 'Image uploaded successfully!' })
|
||
} catch (error) {
|
||
console.error('Error uploading image:', error)
|
||
setToast({ type: 'error', message: 'Error uploading image' })
|
||
} finally {
|
||
setUploadingImage(false)
|
||
}
|
||
}
|
||
|
||
const handleCategorySubmit = async (e) => {
|
||
e.preventDefault()
|
||
|
||
try {
|
||
if (editingCategory) {
|
||
await api.put(`/categories/${editingCategory.id}`, categoryFormData)
|
||
setToast({ type: 'success', message: 'Category updated successfully!' })
|
||
} else {
|
||
await api.post('/categories', categoryFormData)
|
||
setToast({ type: 'success', message: 'Category created successfully!' })
|
||
}
|
||
setShowCategoryForm(false)
|
||
setEditingCategory(null)
|
||
resetCategoryForm()
|
||
fetchCategories()
|
||
} catch (error) {
|
||
console.error('Error saving category:', error)
|
||
setToast({ type: 'error', message: 'Error saving category: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const handleEditCategory = (category) => {
|
||
setEditingCategory(category)
|
||
setCategoryFormData({
|
||
name: category.name || '',
|
||
slug: category.slug || '',
|
||
description: category.description || '',
|
||
image: category.image || '',
|
||
})
|
||
setShowCategoryForm(true)
|
||
setTimeout(() => {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||
}, 100)
|
||
}
|
||
|
||
const handleDeleteCategory = async (id) => {
|
||
if (!confirm('Are you sure you want to delete this category?')) return
|
||
|
||
try {
|
||
await api.delete(`/categories/${id}`)
|
||
setToast({ type: 'success', message: 'Category deleted successfully!' })
|
||
fetchCategories()
|
||
} catch (error) {
|
||
console.error('Error deleting category:', error)
|
||
setToast({ type: 'error', message: 'Error deleting category: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const resetCategoryForm = () => {
|
||
setCategoryFormData({
|
||
name: '',
|
||
slug: '',
|
||
description: '',
|
||
image: '',
|
||
})
|
||
}
|
||
|
||
// Brand management handlers
|
||
const handleAddBrand = async () => {
|
||
const newBrand = brandFormData.name.trim()
|
||
|
||
if (!newBrand) {
|
||
setToast({ type: 'error', message: 'Please enter a brand name' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
await api.post('/brands', { name: newBrand })
|
||
setShowBrandForm(false)
|
||
setBrandFormData({ name: '' })
|
||
fetchBrands()
|
||
setToast({ type: 'success', message: 'Brand added successfully!' })
|
||
} catch (error) {
|
||
console.error('Error adding brand:', error)
|
||
setToast({ type: 'error', message: 'Error adding brand: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const handleUpdateBrand = async () => {
|
||
const oldBrand = editingBrand
|
||
const newBrand = brandFormData.name.trim()
|
||
|
||
if (!newBrand) {
|
||
setToast({ type: 'error', message: 'Please enter a brand name' })
|
||
return
|
||
}
|
||
|
||
if (oldBrand === newBrand) {
|
||
setShowBrandForm(false)
|
||
setEditingBrand(null)
|
||
return
|
||
}
|
||
|
||
if (!confirm(`Update brand from "${oldBrand}" to "${newBrand}"?`)) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
// Find the brand ID
|
||
const brandToUpdate = brandsList.find(b => b.name === oldBrand)
|
||
if (!brandToUpdate) {
|
||
setToast({ type: 'error', message: 'Brand not found' })
|
||
return
|
||
}
|
||
|
||
// Update the brand
|
||
await api.put(`/brands/${brandToUpdate.id}`, { name: newBrand })
|
||
|
||
// Update all products with this brand
|
||
const productsToUpdate = allProducts.filter(p => p.brand === oldBrand)
|
||
for (const product of productsToUpdate) {
|
||
await api.put(`/products/${product.id}`, { ...product, brand: newBrand })
|
||
}
|
||
|
||
// Update all models with this brand
|
||
const modelsToUpdate = models.filter(m => m.brand === oldBrand)
|
||
for (const model of modelsToUpdate) {
|
||
await api.put(`/models/${model.id}`, { ...model, brand: newBrand })
|
||
}
|
||
|
||
setShowBrandForm(false)
|
||
setEditingBrand(null)
|
||
setBrandFormData({ name: '' })
|
||
|
||
// Refresh data
|
||
fetchBrands()
|
||
fetchProducts()
|
||
fetchModels()
|
||
|
||
setToast({ type: 'success', message: 'Brand updated successfully!' })
|
||
} catch (error) {
|
||
console.error('Error updating brand:', error)
|
||
setToast({ type: 'error', message: 'Error updating brand: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const handleDeleteBrand = async (brand) => {
|
||
const productCount = allProducts.filter(p => p.brand === brand).length
|
||
const modelCount = models.filter(m => m.brand === brand).length
|
||
|
||
if (productCount > 0 || modelCount > 0) {
|
||
setToast({ type: 'error', message: `Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).` })
|
||
return
|
||
}
|
||
|
||
if (!confirm(`Are you sure you want to delete the brand "${brand}"?`)) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
// Find the brand ID
|
||
const brandToDelete = brandsList.find(b => b.name === brand)
|
||
if (!brandToDelete) {
|
||
setToast({ type: 'error', message: 'Brand not found' })
|
||
return
|
||
}
|
||
|
||
await api.delete(`/brands/${brandToDelete.id}`)
|
||
fetchBrands()
|
||
setToast({ type: 'success', message: 'Brand deleted successfully!' })
|
||
} catch (error) {
|
||
console.error('Error deleting brand:', error)
|
||
setToast({ type: 'error', message: 'Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
// Model management functions
|
||
const handleModelChange = (e) => {
|
||
const { name, value } = e.target
|
||
setModelFormData({ ...modelFormData, [name]: value })
|
||
}
|
||
|
||
const handleModelSubmit = async (e) => {
|
||
e.preventDefault()
|
||
|
||
const modelData = {
|
||
...modelFormData,
|
||
category_id: parseInt(modelFormData.category_id),
|
||
base_price: modelFormData.base_price ? parseFloat(modelFormData.base_price) : null,
|
||
sizes: modelFormData.sizes ? modelFormData.sizes.split(',').map(s => s.trim()) : [],
|
||
stock: modelFormData.stock ? parseInt(modelFormData.stock) : null,
|
||
}
|
||
|
||
try {
|
||
if (editingModel) {
|
||
await api.put(`/models/${editingModel.id}`, modelData)
|
||
setToast({ type: 'success', message: 'Model updated successfully!' })
|
||
} else {
|
||
await api.post('/models', modelData)
|
||
setToast({ type: 'success', message: 'Model created successfully!' })
|
||
}
|
||
setShowModelForm(false)
|
||
setEditingModel(null)
|
||
resetModelForm()
|
||
fetchModels()
|
||
} catch (error) {
|
||
console.error('Error saving model:', error)
|
||
setToast({ type: 'error', message: 'Error saving model: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const handleModelEdit = (model) => {
|
||
setEditingModel(model)
|
||
setModelFormData({
|
||
name: model.name || '',
|
||
category_id: model.category_id || '',
|
||
brand: model.brand || '',
|
||
base_price: model.base_price || '',
|
||
sizes: Array.isArray(model.sizes) ? model.sizes.join(', ') : '',
|
||
stock: model.stock || '',
|
||
description: model.description || '',
|
||
})
|
||
setShowModelForm(true)
|
||
setTimeout(() => {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||
}, 100)
|
||
}
|
||
|
||
const handleModelDelete = async (id) => {
|
||
if (!confirm('Are you sure you want to delete this model? This will unlink all associated products.')) return
|
||
|
||
try {
|
||
await api.delete(`/models/${id}`)
|
||
setToast({ type: 'success', message: 'Model deleted successfully!' })
|
||
fetchModels()
|
||
} catch (error) {
|
||
console.error('Error deleting model:', error)
|
||
setToast({ type: 'error', message: 'Error deleting model: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const resetModelForm = () => {
|
||
setModelFormData({
|
||
name: '',
|
||
category_id: '',
|
||
brand: '',
|
||
base_price: '',
|
||
sizes: '',
|
||
stock: '',
|
||
description: '',
|
||
})
|
||
}
|
||
|
||
const handleModelCancel = () => {
|
||
setShowModelForm(false)
|
||
setEditingModel(null)
|
||
resetModelForm()
|
||
}
|
||
|
||
// Contact Message Handlers
|
||
const handleUpdateMessage = async (messageId) => {
|
||
try {
|
||
await api.put(`/admin/contact-messages/${messageId}`, {
|
||
status: selectedMessage.status,
|
||
admin_notes: messageNotes,
|
||
is_read: true
|
||
})
|
||
setToast({ type: 'success', message: 'Message updated successfully!' })
|
||
setShowMessageModal(false)
|
||
fetchContactMessages()
|
||
fetchUnreadCount()
|
||
} catch (error) {
|
||
console.error('Error updating message:', error)
|
||
setToast({ type: 'error', message: 'Error updating message: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const handleDeleteMessage = async (messageId) => {
|
||
if (!confirm('Are you sure you want to delete this message?')) return
|
||
|
||
try {
|
||
await api.delete(`/admin/contact-messages/${messageId}`)
|
||
setToast({ type: 'success', message: 'Message deleted successfully!' })
|
||
fetchContactMessages()
|
||
fetchUnreadCount()
|
||
if (showMessageModal) {
|
||
setShowMessageModal(false)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting message:', error)
|
||
setToast({ type: 'error', message: 'Error deleting message: ' + (error.response?.data?.detail || 'Unknown error') })
|
||
}
|
||
}
|
||
|
||
const getCategoryName = (categoryId) => {
|
||
const category = categories.find(c => c.id === categoryId)
|
||
return category ? category.name : 'Unknown'
|
||
}
|
||
|
||
if (!user?.is_admin) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div className="admin-page" style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||
<h1 style={{ marginBottom: '1.5rem' }}>Admin Dashboard</h1>
|
||
|
||
{/* Tab Navigation */}
|
||
<div style={{ marginBottom: '2rem', borderBottom: '2px solid #ddd' }}>
|
||
<button
|
||
onClick={() => setActiveTab('products')}
|
||
style={{
|
||
padding: '0.75rem 1.5rem',
|
||
marginRight: '0.5rem',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'products' ? '3px solid #007bff' : '3px solid transparent',
|
||
backgroundColor: 'transparent',
|
||
cursor: 'pointer',
|
||
fontWeight: activeTab === 'products' ? 'bold' : 'normal',
|
||
fontSize: '1rem'
|
||
}}
|
||
>
|
||
Products
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('categories')}
|
||
style={{
|
||
padding: '0.75rem 1.5rem',
|
||
marginRight: '0.5rem',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'categories' ? '3px solid #007bff' : '3px solid transparent',
|
||
backgroundColor: 'transparent',
|
||
cursor: 'pointer',
|
||
fontWeight: activeTab === 'categories' ? 'bold' : 'normal',
|
||
fontSize: '1rem'
|
||
}}
|
||
>
|
||
Categories
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('brands')}
|
||
style={{
|
||
padding: '0.75rem 1.5rem',
|
||
marginRight: '0.5rem',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'brands' ? '3px solid #007bff' : '3px solid transparent',
|
||
backgroundColor: 'transparent',
|
||
cursor: 'pointer',
|
||
fontWeight: activeTab === 'brands' ? 'bold' : 'normal',
|
||
fontSize: '1rem'
|
||
}}
|
||
>
|
||
Brands
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('models')}
|
||
style={{
|
||
padding: '0.75rem 1.5rem',
|
||
marginRight: '0.5rem',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'models' ? '3px solid #007bff' : '3px solid transparent',
|
||
backgroundColor: 'transparent',
|
||
cursor: 'pointer',
|
||
fontWeight: activeTab === 'models' ? 'bold' : 'normal',
|
||
fontSize: '1rem'
|
||
}}
|
||
>
|
||
Models
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('messages')}
|
||
style={{
|
||
padding: '0.75rem 1.5rem',
|
||
marginRight: '0.5rem',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'messages' ? '3px solid #007bff' : '3px solid transparent',
|
||
backgroundColor: 'transparent',
|
||
cursor: 'pointer',
|
||
fontWeight: activeTab === 'messages' ? 'bold' : 'normal',
|
||
fontSize: '1rem',
|
||
position: 'relative'
|
||
}}
|
||
>
|
||
Contact Messages
|
||
{unreadCount > 0 && (
|
||
<span style={{
|
||
position: 'absolute',
|
||
top: '5px',
|
||
right: '5px',
|
||
backgroundColor: '#dc3545',
|
||
color: 'white',
|
||
borderRadius: '50%',
|
||
padding: '2px 6px',
|
||
fontSize: '0.7rem',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{unreadCount}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Products Section */}
|
||
{activeTab === 'products' && (
|
||
<>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||
<h2>Manage Products</h2>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => {
|
||
setShowForm(!showForm)
|
||
setEditingProduct(null)
|
||
resetForm()
|
||
}}
|
||
>
|
||
{showForm ? 'Cancel' : '+ Add New Product'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Search and Filters */}
|
||
<div style={{
|
||
backgroundColor: '#f8f9fa',
|
||
padding: '1.5rem',
|
||
borderRadius: '8px',
|
||
marginBottom: '2rem',
|
||
display: 'grid',
|
||
gridTemplateColumns: '2fr 1fr 1fr 1fr',
|
||
gap: '1rem',
|
||
alignItems: 'end'
|
||
}}>
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>Search</label>
|
||
<input
|
||
type="text"
|
||
placeholder="Search by name, brand, description..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.5rem',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>Brand</label>
|
||
<select
|
||
value={filterBrand}
|
||
onChange={(e) => setFilterBrand(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.5rem',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px'
|
||
}}
|
||
>
|
||
<option value="">All Brands</option>
|
||
{brands.map(brand => (
|
||
<option key={brand} value={brand}>{brand}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>Category</label>
|
||
<select
|
||
value={filterCategory}
|
||
onChange={(e) => setFilterCategory(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.5rem',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px'
|
||
}}
|
||
>
|
||
<option value="">All Categories</option>
|
||
{categories.map(cat => (
|
||
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>Model</label>
|
||
<select
|
||
value={filterModel}
|
||
onChange={(e) => setFilterModel(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.5rem',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px'
|
||
}}
|
||
>
|
||
<option value="">All Models</option>
|
||
{models.map(model => (
|
||
<option key={model.id} value={model.id}>
|
||
{model.brand} {model.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{showForm && (
|
||
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
|
||
<h2>{editingProduct ? 'Edit Product' : 'Create New Product'}</h2>
|
||
<form onSubmit={handleSubmit}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||
<div className="form-group">
|
||
<label>Name *</label>
|
||
<input type="text" name="name" value={formData.name} onChange={handleChange} required />
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Brand</label>
|
||
<select name="brand" value={formData.brand} onChange={handleChange}>
|
||
<option value="">Select Brand</option>
|
||
{brands.map(brand => (
|
||
<option key={brand} value={brand}>{brand}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>SEO-Friendly URL (Slug)</label>
|
||
<input
|
||
type="text"
|
||
name="slug"
|
||
value={formData.slug}
|
||
onChange={handleChange}
|
||
placeholder="Leave empty to auto-generate from brand and name"
|
||
style={{ backgroundColor: '#f9f9f9' }}
|
||
/>
|
||
{(formData.brand || formData.name) && !formData.slug && (
|
||
<small style={{ color: '#666', marginTop: '0.25rem', display: 'block' }}>
|
||
Will generate: {(formData.brand + ' ' + formData.name).toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-')}
|
||
</small>
|
||
)}
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>Description</label>
|
||
<textarea name="description" value={formData.description} onChange={handleChange} rows="3" />
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>
|
||
Price {formData.model_id && models.find(m => m.id === parseInt(formData.model_id))?.base_price ? '(Optional - inherits from model)' : '*'}
|
||
</label>
|
||
<input
|
||
type="number"
|
||
step="0.01"
|
||
name="price"
|
||
value={formData.price}
|
||
onChange={handleChange}
|
||
required={!formData.model_id || !models.find(m => m.id === parseInt(formData.model_id))?.base_price}
|
||
placeholder={
|
||
formData.model_id && models.find(m => m.id === parseInt(formData.model_id))?.base_price
|
||
? `Model price: ₪${parseFloat(models.find(m => m.id === parseInt(formData.model_id)).base_price).toFixed(2)}`
|
||
: 'Enter price'
|
||
}
|
||
/>
|
||
{formData.model_id && models.find(m => m.id === parseInt(formData.model_id))?.base_price && (
|
||
<small style={{ color: '#666', fontSize: '0.85em' }}>
|
||
Leave empty to use model's base price of ₪{parseFloat(models.find(m => m.id === parseInt(formData.model_id)).base_price).toFixed(2)}
|
||
</small>
|
||
)}
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Discount Price</label>
|
||
<input type="number" step="0.01" name="discount_price" value={formData.discount_price} onChange={handleChange} />
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Category *</label>
|
||
<select name="category_id" value={formData.category_id} onChange={handleChange} required>
|
||
<option value="">Select category</option>
|
||
{categories.map(cat => (
|
||
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Model (Optional - for shared attributes)</label>
|
||
<select name="model_id" value={formData.model_id} onChange={handleChange}>
|
||
<option value="">None - Standalone Product</option>
|
||
{models
|
||
.filter(m => {
|
||
const categoryMatch = !formData.category_id || m.category_id === parseInt(formData.category_id)
|
||
const brandMatch = !formData.brand || m.brand === formData.brand
|
||
return categoryMatch && brandMatch
|
||
})
|
||
.map(model => (
|
||
<option key={model.id} value={model.id}>
|
||
{model.brand} {model.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<small style={{ color: '#666', fontSize: '0.85em' }}>
|
||
{formData.brand ? `Showing models for ${formData.brand}` : 'Select a brand to see related models'}
|
||
</small>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Gender</label>
|
||
<select name="gender" value={formData.gender} onChange={handleChange}>
|
||
<option value="men">Men</option>
|
||
<option value="women">Women</option>
|
||
<option value="unisex">Unisex</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Stock (Optional)</label>
|
||
<input type="number" name="stock" value={formData.stock} onChange={handleChange} placeholder="Leave empty to hide from customers" />
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>
|
||
Override Price (Leave empty to use model default)
|
||
{formData.model_id && <span style={{ color: '#007bff', marginLeft: '0.5rem' }}>Using Model Default</span>}
|
||
</label>
|
||
<input
|
||
type="number"
|
||
step="0.01"
|
||
name="override_price"
|
||
value={formData.override_price}
|
||
onChange={handleChange}
|
||
placeholder={formData.model_id ? "Model default will be used" : "Override price for this product"}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>
|
||
Sizes (comma separated)
|
||
{formData.model_id ? ' - Override model default' : ' *'}
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="sizes"
|
||
value={formData.sizes}
|
||
onChange={handleChange}
|
||
placeholder={formData.model_id ? "Leave empty to use model sizes" : "S, M, L, XL"}
|
||
required={!formData.model_id}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>Product Images</label>
|
||
|
||
{/* Image Upload - Multiple Files */}
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={handleImageUpload}
|
||
disabled={uploadingImage}
|
||
style={{ display: 'block', marginBottom: '0.5rem' }}
|
||
/>
|
||
<small style={{ color: '#666', fontSize: '0.85em' }}>
|
||
Tip: Select multiple images or upload a folder
|
||
</small>
|
||
{uploadingImage && <p style={{ color: '#666' }}>Uploading images...</p>}
|
||
</div>
|
||
|
||
{/* Image Preview */}
|
||
{formData.images && (
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '1rem' }}>
|
||
{formData.images.split(',').map((img, idx) => {
|
||
const imgUrl = img.trim()
|
||
if (!imgUrl) return null
|
||
return (
|
||
<div key={idx} style={{ position: 'relative', border: '1px solid #ddd', borderRadius: '4px', padding: '0.5rem' }}>
|
||
<img
|
||
src={imgUrl}
|
||
alt={`Product ${idx + 1}`}
|
||
style={{ width: '100%', height: '100px', objectFit: 'cover', borderRadius: '4px' }}
|
||
onError={(e) => {
|
||
e.target.src = 'https://via.placeholder.com/100?text=Invalid'
|
||
}}
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeImage(imgUrl)}
|
||
style={{
|
||
position: 'absolute',
|
||
top: '0.25rem',
|
||
right: '0.25rem',
|
||
background: 'red',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '50%',
|
||
width: '24px',
|
||
height: '24px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
lineHeight: '1'
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{/* Manual URL input (optional) */}
|
||
<details style={{ marginTop: '1rem' }}>
|
||
<summary style={{ cursor: 'pointer', color: '#666' }}>Or add image URLs manually</summary>
|
||
<input
|
||
type="text"
|
||
name="images"
|
||
value={formData.images}
|
||
onChange={handleChange}
|
||
placeholder="https://example.com/image1.jpg, https://..."
|
||
style={{ marginTop: '0.5rem', width: '100%' }}
|
||
/>
|
||
</details>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<input type="checkbox" name="is_featured" checked={formData.is_featured} onChange={handleChange} />
|
||
Featured Product
|
||
</label>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<input type="checkbox" name="is_on_sale" checked={formData.is_on_sale} onChange={handleChange} />
|
||
On Sale
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||
<button type="submit" className="btn btn-primary">
|
||
{editingProduct ? 'Update Product' : 'Create Product'}
|
||
</button>
|
||
<button type="button" className="btn" onClick={handleCancel} style={{ background: '#666' }}>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<h2>Products ({products.length})</h2>
|
||
{loading ? (
|
||
<p>Loading...</p>
|
||
) : (
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
|
||
<thead>
|
||
<tr style={{ backgroundColor: '#f0f0f0', textAlign: 'left' }}>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>ID</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Name</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Brand</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Slug</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Price</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Stock</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Featured</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>On Sale</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{products.map(product => (
|
||
<tr key={product.id}>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.id}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.name}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.brand}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em', color: '#666' }}>
|
||
{product.slug || '-'}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>₪{product.price}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.stock ?? 'Hidden'}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.is_featured ? '✓' : '-'}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.is_on_sale ? '✓' : '-'}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
<button
|
||
onClick={() => handleEdit(product)}
|
||
style={{ marginRight: '0.5rem', padding: '0.25rem 0.75rem', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleDelete(product.id)}
|
||
style={{ padding: '0.25rem 0.75rem', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
Delete
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Categories Section */}
|
||
{activeTab === 'categories' && (
|
||
<>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||
<h2>Manage Categories</h2>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => {
|
||
setShowCategoryForm(!showCategoryForm)
|
||
setEditingCategory(null)
|
||
resetCategoryForm()
|
||
}}
|
||
>
|
||
{showCategoryForm ? 'Cancel' : '+ Add New Category'}
|
||
</button>
|
||
</div>
|
||
|
||
{showCategoryForm && (
|
||
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
|
||
<h2>{editingCategory ? 'Edit Category' : 'Create New Category'}</h2>
|
||
<form onSubmit={handleCategorySubmit}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||
<div className="form-group">
|
||
<label>Name *</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={categoryFormData.name}
|
||
onChange={handleCategoryChange}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Slug *</label>
|
||
<input
|
||
type="text"
|
||
name="slug"
|
||
value={categoryFormData.slug}
|
||
onChange={handleCategoryChange}
|
||
placeholder="e.g., shoes, shirts, pants"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>Description</label>
|
||
<textarea
|
||
name="description"
|
||
value={categoryFormData.description}
|
||
onChange={handleCategoryChange}
|
||
rows="3"
|
||
placeholder="Category description"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>Category Image</label>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleCategoryImageUpload}
|
||
disabled={uploadingImage}
|
||
style={{ display: 'block', marginBottom: '0.5rem' }}
|
||
/>
|
||
{uploadingImage && <p style={{ color: '#666' }}>Uploading image...</p>}
|
||
{categoryFormData.image && (
|
||
<div style={{ marginTop: '1rem' }}>
|
||
<img
|
||
src={categoryFormData.image}
|
||
alt="Category preview"
|
||
style={{ maxWidth: '200px', maxHeight: '200px', objectFit: 'cover', borderRadius: '8px' }}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginTop: '1.5rem' }}>
|
||
<button type="submit" className="btn btn-primary" style={{ marginRight: '1rem' }}>
|
||
{editingCategory ? 'Update Category' : 'Create Category'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary"
|
||
onClick={() => {
|
||
setShowCategoryForm(false)
|
||
setEditingCategory(null)
|
||
resetCategoryForm()
|
||
}}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<h3>Categories ({categories.length})</h3>
|
||
{loading ? (
|
||
<p>Loading...</p>
|
||
) : (
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '1.5rem', marginTop: '1rem' }}>
|
||
{categories.map(category => (
|
||
<div key={category.id} style={{ border: '1px solid #ddd', borderRadius: '8px', padding: '1rem', backgroundColor: 'white' }}>
|
||
{category.image && (
|
||
<img
|
||
src={category.image}
|
||
alt={category.name}
|
||
style={{ width: '100%', height: '150px', objectFit: 'cover', borderRadius: '4px', marginBottom: '1rem' }}
|
||
/>
|
||
)}
|
||
<h4 style={{ margin: '0.5rem 0' }}>{category.name}</h4>
|
||
<p style={{ fontSize: '0.85em', color: '#666', margin: '0.25rem 0' }}>Slug: {category.slug}</p>
|
||
{category.description && (
|
||
<p style={{ fontSize: '0.9em', color: '#555', margin: '0.5rem 0' }}>{category.description}</p>
|
||
)}
|
||
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
|
||
<button
|
||
onClick={() => handleEditCategory(category)}
|
||
style={{ flex: 1, padding: '0.5rem', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteCategory(category.id)}
|
||
style={{ flex: 1, padding: '0.5rem', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Brands Section */}
|
||
{activeTab === 'brands' && (
|
||
<>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||
<h2>Manage Brands</h2>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => {
|
||
setShowBrandForm(!showBrandForm)
|
||
setEditingBrand(null)
|
||
setBrandFormData({ name: '' })
|
||
}}
|
||
>
|
||
{showBrandForm ? 'Cancel' : '+ Add New Brand'}
|
||
</button>
|
||
</div>
|
||
|
||
{showBrandForm && (
|
||
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
|
||
<h2>{editingBrand ? 'Edit Brand' : 'Add New Brand'}</h2>
|
||
<form onSubmit={(e) => {
|
||
e.preventDefault()
|
||
if (editingBrand) {
|
||
handleUpdateBrand()
|
||
} else {
|
||
handleAddBrand()
|
||
}
|
||
}}>
|
||
<div className="form-group">
|
||
<label>Brand Name *</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={brandFormData.name}
|
||
onChange={(e) => setBrandFormData({ name: e.target.value })}
|
||
placeholder="e.g., Nike, Adidas, New Balance"
|
||
required
|
||
style={{ width: '100%', maxWidth: '400px' }}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ marginTop: '1.5rem' }}>
|
||
<button type="submit" className="btn btn-primary" style={{ marginRight: '1rem' }}>
|
||
{editingBrand ? 'Update Brand' : 'Add Brand'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary"
|
||
onClick={() => {
|
||
setShowBrandForm(false)
|
||
setEditingBrand(null)
|
||
setBrandFormData({ name: '' })
|
||
}}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<h3>Brands ({brands.length})</h3>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '1rem', marginTop: '1rem' }}>
|
||
{brands.map((brand, index) => {
|
||
const productCount = allProducts.filter(p => p.brand === brand).length
|
||
const modelCount = models.filter(m => m.brand === brand).length
|
||
|
||
return (
|
||
<div key={index} style={{
|
||
border: '1px solid #ddd',
|
||
borderRadius: '8px',
|
||
padding: '1rem',
|
||
backgroundColor: 'white',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'space-between'
|
||
}}>
|
||
<div>
|
||
<h4 style={{ margin: '0 0 0.5rem 0' }}>{brand}</h4>
|
||
<p style={{ fontSize: '0.85em', color: '#666', margin: '0.25rem 0' }}>
|
||
{productCount} product{productCount !== 1 ? 's' : ''}
|
||
</p>
|
||
<p style={{ fontSize: '0.85em', color: '#666', margin: '0.25rem 0' }}>
|
||
{modelCount} model{modelCount !== 1 ? 's' : ''}
|
||
</p>
|
||
</div>
|
||
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
|
||
<button
|
||
onClick={() => {
|
||
setEditingBrand(brand)
|
||
setBrandFormData({ name: brand })
|
||
setShowBrandForm(true)
|
||
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 100)
|
||
}}
|
||
style={{ flex: 1, padding: '0.5rem', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteBrand(brand)}
|
||
style={{ flex: 1, padding: '0.5rem', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||
disabled={productCount > 0 || modelCount > 0}
|
||
title={productCount > 0 || modelCount > 0 ? 'Cannot delete brand in use' : 'Delete brand'}
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Models Section */}
|
||
{activeTab === 'models' && (
|
||
<>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||
<h2>Manage Models</h2>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => {
|
||
setShowModelForm(!showModelForm)
|
||
setEditingModel(null)
|
||
resetModelForm()
|
||
}}
|
||
>
|
||
{showModelForm ? 'Cancel' : '+ Add New Model'}
|
||
</button>
|
||
</div>
|
||
|
||
{showModelForm && (
|
||
<div style={{ backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px', marginBottom: '2rem' }}>
|
||
<h2>{editingModel ? 'Edit Model' : 'Create New Model'}</h2>
|
||
<form onSubmit={handleModelSubmit}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||
<div className="form-group">
|
||
<label>Model Name * (e.g., 9060, Air Max 90)</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={modelFormData.name}
|
||
onChange={handleModelChange}
|
||
placeholder="9060"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Brand *</label>
|
||
<select
|
||
name="brand"
|
||
value={modelFormData.brand}
|
||
onChange={handleModelChange}
|
||
required
|
||
>
|
||
<option value="">Select Brand</option>
|
||
{brands.map(brand => (
|
||
<option key={brand} value={brand}>{brand}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Category *</label>
|
||
<select name="category_id" value={modelFormData.category_id} onChange={handleModelChange} required>
|
||
<option value="">Select Category</option>
|
||
{categories.map(category => (
|
||
<option key={category.id} value={category.id}>
|
||
{category.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Base Price (Default for all products)</label>
|
||
<input
|
||
type="number"
|
||
step="0.01"
|
||
name="base_price"
|
||
value={modelFormData.base_price}
|
||
onChange={handleModelChange}
|
||
placeholder="129.99"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Stock (Optional)</label>
|
||
<input
|
||
type="number"
|
||
name="stock"
|
||
value={modelFormData.stock}
|
||
onChange={handleModelChange}
|
||
placeholder="0"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>Default Sizes (comma separated)</label>
|
||
<input
|
||
type="text"
|
||
name="sizes"
|
||
value={modelFormData.sizes}
|
||
onChange={handleModelChange}
|
||
placeholder="7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||
<label>Description</label>
|
||
<textarea
|
||
name="description"
|
||
value={modelFormData.description}
|
||
onChange={handleModelChange}
|
||
rows="4"
|
||
placeholder="Model description..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginTop: '1.5rem' }}>
|
||
<button type="submit" className="btn btn-primary" style={{ marginRight: '1rem' }}>
|
||
{editingModel ? 'Update Model' : 'Create Model'}
|
||
</button>
|
||
<button type="button" className="btn btn-secondary" onClick={handleModelCancel}>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<h2>Models ({models.length})</h2>
|
||
{loading ? (
|
||
<p>Loading...</p>
|
||
) : (
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
|
||
<thead>
|
||
<tr style={{ backgroundColor: '#f0f0f0', textAlign: 'left' }}>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>ID</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Model Name</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Brand</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Category</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Base Price</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Stock</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Sizes</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{models.map(model => (
|
||
<tr key={model.id}>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.id}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.name}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{model.brand}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{getCategoryName(model.category_id)}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
{model.base_price ? `₪${parseFloat(model.base_price).toFixed(2)}` : '-'}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
{model.stock ?? '-'}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
|
||
{model.sizes ? model.sizes.join(', ') : '-'}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
<button
|
||
onClick={() => handleModelEdit(model)}
|
||
style={{
|
||
marginRight: '0.5rem',
|
||
padding: '0.25rem 0.75rem',
|
||
backgroundColor: '#4CAF50',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleModelDelete(model.id)}
|
||
style={{
|
||
padding: '0.25rem 0.75rem',
|
||
backgroundColor: '#f44336',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Delete
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Contact Messages Section */}
|
||
{activeTab === 'messages' && (
|
||
<>
|
||
<div style={{ marginBottom: '2rem' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||
<h2>Contact Messages</h2>
|
||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||
<select
|
||
value={messageFilter}
|
||
onChange={(e) => setMessageFilter(e.target.value)}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
borderRadius: '4px',
|
||
border: '1px solid #ddd'
|
||
}}
|
||
>
|
||
<option value="all">All Messages</option>
|
||
<option value="new">New</option>
|
||
<option value="read">Read</option>
|
||
<option value="replied">Replied</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{filteredMessages.length === 0 ? (
|
||
<p style={{ textAlign: 'center', color: '#666' }}>No messages found.</p>
|
||
) : (
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', backgroundColor: 'white', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
|
||
<thead>
|
||
<tr style={{ backgroundColor: '#f8f9fa' }}>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>ID</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Name</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Email</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Phone</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Subject</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Date</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Status</th>
|
||
<th style={{ padding: '0.75rem', border: '1px solid #ddd', textAlign: 'left' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filteredMessages.map(msg => (
|
||
<tr
|
||
key={msg.id}
|
||
style={{
|
||
backgroundColor: !msg.is_read ? '#fff3cd' : 'white',
|
||
cursor: 'pointer'
|
||
}}
|
||
onClick={() => {
|
||
setSelectedMessage(msg)
|
||
setMessageNotes(msg.admin_notes || '')
|
||
setShowMessageModal(true)
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
{!msg.is_read && <span style={{ color: '#dc3545', marginRight: '5px', fontWeight: 'bold' }}>●</span>}
|
||
{msg.id}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{msg.full_name}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{msg.email}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{msg.phone || '-'}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{msg.subject}</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
{new Date(msg.created_at).toLocaleDateString()}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
<span style={{
|
||
padding: '0.25rem 0.5rem',
|
||
borderRadius: '12px',
|
||
fontSize: '0.85rem',
|
||
fontWeight: 'bold',
|
||
backgroundColor: msg.status === 'new' ? '#dc3545' : msg.status === 'read' ? '#ffc107' : '#28a745',
|
||
color: 'white'
|
||
}}>
|
||
{msg.status.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
handleDeleteMessage(msg.id)
|
||
}}
|
||
style={{
|
||
padding: '0.25rem 0.75rem',
|
||
backgroundColor: '#f44336',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Delete
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* Message Modal */}
|
||
{showMessageModal && selectedMessage && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
zIndex: 1000
|
||
}}>
|
||
<div style={{
|
||
backgroundColor: 'white',
|
||
padding: '2rem',
|
||
borderRadius: '8px',
|
||
maxWidth: '600px',
|
||
width: '90%',
|
||
maxHeight: '80vh',
|
||
overflowY: 'auto'
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
|
||
<h2>Message Details</h2>
|
||
<button
|
||
onClick={() => setShowMessageModal(false)}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
backgroundColor: '#6c757d',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<strong>From:</strong> {selectedMessage.full_name}
|
||
</div>
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<strong>Email:</strong> {selectedMessage.email}
|
||
</div>
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<strong>Phone:</strong> {selectedMessage.phone || 'Not provided'}
|
||
</div>
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<strong>Subject:</strong> {selectedMessage.subject}
|
||
</div>
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<strong>Date:</strong> {new Date(selectedMessage.created_at).toLocaleString()}
|
||
</div>
|
||
<div style={{ marginBottom: '1.5rem' }}>
|
||
<strong>Message:</strong>
|
||
<div style={{
|
||
marginTop: '0.5rem',
|
||
padding: '1rem',
|
||
backgroundColor: '#f8f9fa',
|
||
borderRadius: '4px',
|
||
whiteSpace: 'pre-wrap'
|
||
}}>
|
||
{selectedMessage.message}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}>
|
||
Status:
|
||
</label>
|
||
<select
|
||
value={selectedMessage.status}
|
||
onChange={(e) => setSelectedMessage({ ...selectedMessage, status: e.target.value })}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.5rem',
|
||
borderRadius: '4px',
|
||
border: '1px solid #ddd'
|
||
}}
|
||
>
|
||
<option value="new">New</option>
|
||
<option value="read">Read</option>
|
||
<option value="replied">Replied</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '1.5rem' }}>
|
||
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold' }}>
|
||
Admin Notes:
|
||
</label>
|
||
<textarea
|
||
value={messageNotes}
|
||
onChange={(e) => setMessageNotes(e.target.value)}
|
||
placeholder="Add internal notes about this message..."
|
||
rows="4"
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.5rem',
|
||
borderRadius: '4px',
|
||
border: '1px solid #ddd',
|
||
resize: 'vertical'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||
<button
|
||
onClick={() => handleUpdateMessage(selectedMessage.id)}
|
||
style={{
|
||
flex: 1,
|
||
padding: '0.75rem',
|
||
backgroundColor: '#28a745',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontWeight: 'bold'
|
||
}}
|
||
>
|
||
Save Changes
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
handleDeleteMessage(selectedMessage.id)
|
||
setShowMessageModal(false)
|
||
}}
|
||
style={{
|
||
padding: '0.75rem 1.5rem',
|
||
backgroundColor: '#dc3545',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{toast && (
|
||
<Toast
|
||
message={toast.message}
|
||
type={toast.type}
|
||
onClose={() => setToast(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|