416 lines
19 KiB
React
416 lines
19 KiB
React
|
|
import React, { useState, useEffect } from 'react'
|
||
|
|
import { Popconfirm, message } from 'antd'
|
||
|
|
import { useAdminPanel } from '../AdminPanelContext'
|
||
|
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||
|
|
import "../Styles/HeroSectionForm.scss"
|
||
|
|
|
||
|
|
const HeroSectionForm = () => {
|
||
|
|
const { sectionData, updateSectionData } = useAdminPanel()
|
||
|
|
const heroData = sectionData.HeroSectionForm || []
|
||
|
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||
|
|
const [title, setTitle] = useState('')
|
||
|
|
const [description, setDescription] = useState('')
|
||
|
|
const [subsections, setSubsections] = useState([{ id: 1, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||
|
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||
|
|
const [titleError, setTitleError] = useState('')
|
||
|
|
const [descriptionError, setDescriptionError] = useState('')
|
||
|
|
|
||
|
|
// UUID generator fallback for browser compatibility
|
||
|
|
const generateUUID = () => {
|
||
|
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||
|
|
return crypto.randomUUID()
|
||
|
|
}
|
||
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
|
|
const r = Math.random() * 16 | 0
|
||
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||
|
|
return v.toString(16)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
return () => {
|
||
|
|
subsections.forEach(s => { if (s.previewUrl) URL.revokeObjectURL(s.previewUrl) })
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const openModal = () => {
|
||
|
|
setIsModalOpen(true)
|
||
|
|
}
|
||
|
|
|
||
|
|
const closeModal = () => {
|
||
|
|
setIsModalOpen(false)
|
||
|
|
setTitle('')
|
||
|
|
setDescription('')
|
||
|
|
setSubsections([{ id: 1, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||
|
|
setEditingIndex(-1)
|
||
|
|
setTitleError('')
|
||
|
|
setDescriptionError('')
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleClear = () => {
|
||
|
|
setTitle('')
|
||
|
|
setDescription('')
|
||
|
|
setSubsections([{ id: 1, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||
|
|
setTitleError('')
|
||
|
|
setDescriptionError('')
|
||
|
|
}
|
||
|
|
|
||
|
|
const addSubsection = () => {
|
||
|
|
if (subsections.length >= 3) {
|
||
|
|
message.warning('Only 3 subsections allowed')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const newId = Math.max(...subsections.map(s => s.id)) + 1
|
||
|
|
setSubsections([...subsections, { id: newId, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||
|
|
}
|
||
|
|
|
||
|
|
const removeSubsection = (id) => {
|
||
|
|
if (subsections.length === 1) {
|
||
|
|
message.warning('At least one subsection is required')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const toRemove = subsections.find(s => s.id === id)
|
||
|
|
if (toRemove?.previewUrl) {
|
||
|
|
URL.revokeObjectURL(toRemove.previewUrl)
|
||
|
|
}
|
||
|
|
setSubsections(subsections.filter(s => s.id !== id))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleImageUpload = (id, file) => {
|
||
|
|
setSubsections(subsections.map(s => {
|
||
|
|
if (s.id !== id) return s
|
||
|
|
if (s.previewUrl) {
|
||
|
|
URL.revokeObjectURL(s.previewUrl)
|
||
|
|
}
|
||
|
|
const nextUrl = file ? URL.createObjectURL(file) : null
|
||
|
|
return { ...s, image: file || null, previewUrl: nextUrl }
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSubsectionChange = (id, field, value) => {
|
||
|
|
setSubsections(subsections.map(s =>
|
||
|
|
s.id === id ? { ...s, [field]: value } : s
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSave = () => {
|
||
|
|
let hasError = false
|
||
|
|
|
||
|
|
if (!title.trim()) {
|
||
|
|
setTitleError('Title is required')
|
||
|
|
hasError = true
|
||
|
|
} else {
|
||
|
|
setTitleError('')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!description.trim()) {
|
||
|
|
setDescriptionError('Description is required')
|
||
|
|
hasError = true
|
||
|
|
} else {
|
||
|
|
setDescriptionError('')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hasError) {
|
||
|
|
message.error('Please fix the highlighted fields')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const newHero = {
|
||
|
|
id: editingIndex >= 0 ? heroData[editingIndex].id : generateUUID(),
|
||
|
|
title: title.trim(),
|
||
|
|
description: description.trim(),
|
||
|
|
subsections: subsections.map(s => ({
|
||
|
|
id: s.id,
|
||
|
|
heading: s.heading,
|
||
|
|
subheading: s.subheading,
|
||
|
|
displayUrl: s.previewUrl || (s.image ? URL.createObjectURL(s.image) : null),
|
||
|
|
})),
|
||
|
|
active: true,
|
||
|
|
createdAt: editingIndex >= 0 ? heroData[editingIndex].createdAt : new Date().toLocaleDateString()
|
||
|
|
}
|
||
|
|
|
||
|
|
if (editingIndex >= 0) {
|
||
|
|
const updatedHeros = heroData.map((hero, index) => index === editingIndex ? newHero : hero)
|
||
|
|
updateSectionData('HeroSectionForm', updatedHeros)
|
||
|
|
setEditingIndex(-1)
|
||
|
|
message.success('Hero section updated!')
|
||
|
|
} else {
|
||
|
|
updateSectionData('HeroSectionForm', [...heroData, newHero])
|
||
|
|
message.success('Hero section added!')
|
||
|
|
}
|
||
|
|
|
||
|
|
closeModal()
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDelete = (index) => {
|
||
|
|
const updatedHeros = heroData.filter((_, i) => i !== index)
|
||
|
|
updateSectionData('HeroSectionForm', updatedHeros)
|
||
|
|
message.success('Hero section deleted')
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleEdit = (index) => {
|
||
|
|
setTitle(heroData[index].title)
|
||
|
|
setDescription(heroData[index].description)
|
||
|
|
if (heroData[index].subsections && heroData[index].subsections.length > 0) {
|
||
|
|
setSubsections(heroData[index].subsections.map(ss => ({
|
||
|
|
id: ss.id,
|
||
|
|
image: null,
|
||
|
|
previewUrl: ss.displayUrl || null,
|
||
|
|
heading: ss.heading || '',
|
||
|
|
subheading: ss.subheading || ''
|
||
|
|
})))
|
||
|
|
}
|
||
|
|
setEditingIndex(index)
|
||
|
|
openModal()
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleToggleActive = (index) => {
|
||
|
|
const updated = [...heroData]
|
||
|
|
updated[index].active = !updated[index].active
|
||
|
|
updateSectionData('HeroSectionForm', updated)
|
||
|
|
message.success('Status updated')
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className='hero-section-form-master'>
|
||
|
|
{/* Header Section */}
|
||
|
|
<div className='hero-section-form-header'>
|
||
|
|
<div className='hero-section-header-left'>
|
||
|
|
<div className='hero-section-header-content'>
|
||
|
|
<h2>Hero Section Management</h2>
|
||
|
|
<p>Manage and organize your hero section content and settings.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button onClick={openModal} className='hero-section-create-btn'>
|
||
|
|
<span>+</span> Create Hero Section
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Tab Navigation */}
|
||
|
|
<div className='hero-section-tab-navigation'>
|
||
|
|
<div className='hero-section-tab-buttons'>
|
||
|
|
<button className='hero-section-tab-btn hero-section-active'>
|
||
|
|
Hero Sections ({heroData.length})
|
||
|
|
</button>
|
||
|
|
<button className='hero-section-tab-btn'>
|
||
|
|
All Status
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className='hero-section-tab-controls'>
|
||
|
|
<div className='hero-section-sort-dropdown'>
|
||
|
|
<select>
|
||
|
|
<option value="newest">Newest First</option>
|
||
|
|
<option value="oldest">Oldest First</option>
|
||
|
|
<option value="alphabetical">Alphabetical</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div className='hero-section-view-toggle'>
|
||
|
|
<button className='hero-section-view-btn hero-section-active'>
|
||
|
|
<span>⊞</span>
|
||
|
|
</button>
|
||
|
|
<button className='hero-section-view-btn'>
|
||
|
|
<span>☰</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content Area */}
|
||
|
|
{heroData.length > 0 ? (
|
||
|
|
<div className='hero-section-display'>
|
||
|
|
<div className='hero-section-grid'>
|
||
|
|
{heroData.map((hero, index) => (
|
||
|
|
<div key={hero.id} className={`hero-section-card ${!hero.active ? 'hero-section-inactive' : ''}`}>
|
||
|
|
<div className='hero-section-card-header'>
|
||
|
|
<h3>{hero.title}</h3>
|
||
|
|
<div className='hero-section-card-actions'>
|
||
|
|
<button className='hero-section-edit-btn' onClick={() => handleEdit(index)}>Edit</button>
|
||
|
|
<Popconfirm
|
||
|
|
title="Delete Hero Section"
|
||
|
|
description="Are you sure you want to delete this hero section?"
|
||
|
|
onConfirm={() => handleDelete(index)}
|
||
|
|
okText="Yes"
|
||
|
|
cancelText="No"
|
||
|
|
>
|
||
|
|
<button className='hero-section-delete-btn'>Delete</button>
|
||
|
|
</Popconfirm>
|
||
|
|
<button
|
||
|
|
className={`hero-section-status-btn ${hero.active ? 'hero-section-deactivate' : 'hero-section-activate'}`}
|
||
|
|
onClick={() => handleToggleActive(index)}
|
||
|
|
>
|
||
|
|
{hero.active ? 'Deactivate' : 'Activate'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className='hero-section-card-content'>
|
||
|
|
<p><strong>Description:</strong> {hero.description || 'No description'}</p>
|
||
|
|
<p className='hero-section-created-date'>Created: {hero.date || hero.createdAt}</p>
|
||
|
|
<p className={`hero-section-status ${hero.active ? 'hero-section-active' : 'hero-section-inactive'}`}>
|
||
|
|
Status: {hero.active ? 'Active' : 'Inactive'}
|
||
|
|
</p>
|
||
|
|
{hero.subsections && hero.subsections.length > 0 && (
|
||
|
|
<div className='hero-section-subsections'>
|
||
|
|
<strong>Subsections:</strong>
|
||
|
|
{hero.subsections.map(ss => (
|
||
|
|
<div key={ss.id} className='hero-section-subsection-item'>
|
||
|
|
{ss.displayUrl && (
|
||
|
|
<div className='hero-section-thumb'>
|
||
|
|
<img src={ss.displayUrl} alt={ss.heading || 'Subsection'} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
<div className='hero-section-texts'>
|
||
|
|
{ss.heading && <div className='hero-section-heading'>{ss.heading}</div>}
|
||
|
|
{ss.subheading && <div className='hero-section-subheading'>{ss.subheading}</div>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className='hero-section-placeholder-content'>
|
||
|
|
<div className='hero-section-placeholder-icon'>
|
||
|
|
<span>🎯</span>
|
||
|
|
</div>
|
||
|
|
<h2>No Hero Sections Found</h2>
|
||
|
|
<p>Get started by creating your first hero section</p>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
{/* Modal */}
|
||
|
|
<DefaultModal
|
||
|
|
open={isModalOpen}
|
||
|
|
title={editingIndex >= 0 ? 'Edit Hero Section' : 'Create New Hero Section'}
|
||
|
|
handleCancel={closeModal}
|
||
|
|
handleSubmit={handleSave}
|
||
|
|
buttonText={editingIndex >= 0 ? 'Update Hero' : 'Add Hero'}
|
||
|
|
width={700}
|
||
|
|
destroyOnClose={true}
|
||
|
|
>
|
||
|
|
<div className='hero-section-form'>
|
||
|
|
<div className='hero-section-form-group'>
|
||
|
|
<label htmlFor='heroTitle'>Title</label>
|
||
|
|
<input
|
||
|
|
type='text'
|
||
|
|
id='heroTitle'
|
||
|
|
value={title}
|
||
|
|
onChange={(e) => {
|
||
|
|
setTitle(e.target.value)
|
||
|
|
if (titleError) setTitleError('')
|
||
|
|
}}
|
||
|
|
placeholder='Enter hero title'
|
||
|
|
className={titleError ? 'error' : ''}
|
||
|
|
autoFocus
|
||
|
|
/>
|
||
|
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className='hero-section-form-group'>
|
||
|
|
<label htmlFor='heroDescription'>Description</label>
|
||
|
|
<textarea
|
||
|
|
id='heroDescription'
|
||
|
|
value={description}
|
||
|
|
onChange={(e) => {
|
||
|
|
setDescription(e.target.value)
|
||
|
|
if (descriptionError) setDescriptionError('')
|
||
|
|
}}
|
||
|
|
placeholder='Enter hero description'
|
||
|
|
rows='4'
|
||
|
|
className={descriptionError ? 'error' : ''}
|
||
|
|
/>
|
||
|
|
{descriptionError && <span className='error-message'>{descriptionError}</span>}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className='hero-section-subsections-container'>
|
||
|
|
<div className='hero-section-subsections-header'>
|
||
|
|
<h3>Subsections</h3>
|
||
|
|
<p>Add up to 3 subsections with images and content</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{subsections.map((subsection, idx) => (
|
||
|
|
<div key={subsection.id} className='hero-section-subsection'>
|
||
|
|
<div className='hero-section-subsection-number'>
|
||
|
|
Subsection {idx + 1}
|
||
|
|
{subsections.length > 1 && (
|
||
|
|
<button
|
||
|
|
type='button'
|
||
|
|
className='hero-section-remove-subsection-btn'
|
||
|
|
onClick={() => removeSubsection(subsection.id)}
|
||
|
|
>
|
||
|
|
✕
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className='hero-section-form-group'>
|
||
|
|
<label>Image Upload</label>
|
||
|
|
<input
|
||
|
|
type='file'
|
||
|
|
accept='image/*'
|
||
|
|
onChange={(e) => handleImageUpload(subsection.id, e.target.files[0])}
|
||
|
|
className='hero-section-file-input'
|
||
|
|
aria-label={`Upload image for subsection ${idx + 1}`}
|
||
|
|
/>
|
||
|
|
{subsection.previewUrl && (
|
||
|
|
<div className='hero-section-image-preview'>
|
||
|
|
<img src={subsection.previewUrl} alt='Preview' />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className='hero-section-form-group'>
|
||
|
|
<label>Heading</label>
|
||
|
|
<input
|
||
|
|
type='text'
|
||
|
|
placeholder='Enter heading'
|
||
|
|
value={subsection.heading}
|
||
|
|
onChange={(e) => handleSubsectionChange(subsection.id, 'heading', e.target.value)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className='hero-section-form-group'>
|
||
|
|
<label>Sub Heading</label>
|
||
|
|
<input
|
||
|
|
type='text'
|
||
|
|
placeholder='Enter sub heading'
|
||
|
|
value={subsection.subheading}
|
||
|
|
onChange={(e) => handleSubsectionChange(subsection.id, 'subheading', e.target.value)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
|
||
|
|
<button
|
||
|
|
type='button'
|
||
|
|
className='hero-section-add-subsection-btn'
|
||
|
|
onClick={addSubsection}
|
||
|
|
disabled={subsections.length >= 3}
|
||
|
|
>
|
||
|
|
+ Add Subsection
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className='hero-section-form-actions'>
|
||
|
|
<button type='button' onClick={handleSave} className='hero-section-save-btn'>
|
||
|
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||
|
|
</button>
|
||
|
|
<button type='button' onClick={closeModal} className='hero-section-cancel-btn'>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button type='button' onClick={handleClear} className='hero-section-clear-btn'>
|
||
|
|
Clear
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</DefaultModal>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default HeroSectionForm
|