update
This commit is contained in:
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -51,7 +51,7 @@ class PageContentUpdateRequest(BaseModel):
|
|||||||
luxury_features: Optional[Union[str, List[Dict[str, Any]]]] = None
|
luxury_features: Optional[Union[str, List[Dict[str, Any]]]] = None
|
||||||
luxury_gallery_section_title: Optional[str] = Field(None, max_length=500)
|
luxury_gallery_section_title: Optional[str] = Field(None, max_length=500)
|
||||||
luxury_gallery_section_subtitle: Optional[str] = Field(None, max_length=1000)
|
luxury_gallery_section_subtitle: Optional[str] = Field(None, max_length=1000)
|
||||||
luxury_gallery: Optional[Union[str, List[Dict[str, Any]]]] = None
|
luxury_gallery: Optional[Union[str, List[str]]] = None
|
||||||
luxury_testimonials_section_title: Optional[str] = Field(None, max_length=500)
|
luxury_testimonials_section_title: Optional[str] = Field(None, max_length=500)
|
||||||
luxury_testimonials_section_subtitle: Optional[str] = Field(None, max_length=1000)
|
luxury_testimonials_section_subtitle: Optional[str] = Field(None, max_length=1000)
|
||||||
luxury_testimonials: Optional[Union[str, List[Dict[str, Any]]]] = None
|
luxury_testimonials: Optional[Union[str, List[Dict[str, Any]]]] = None
|
||||||
|
|||||||
286
Frontend/src/features/content/components/PartnersCarousel.tsx
Normal file
286
Frontend/src/features/content/components/PartnersCarousel.tsx
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Partner {
|
||||||
|
name?: string;
|
||||||
|
logo?: string;
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PartnersCarouselProps {
|
||||||
|
partners: Partner[];
|
||||||
|
autoSlideInterval?: number;
|
||||||
|
showNavigation?: boolean;
|
||||||
|
itemsPerView?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PartnersCarousel: React.FC<PartnersCarouselProps> = ({
|
||||||
|
partners,
|
||||||
|
autoSlideInterval = 5000,
|
||||||
|
showNavigation = true,
|
||||||
|
itemsPerView = 6,
|
||||||
|
}) => {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1920);
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowWidth(window.innerWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Calculate responsive items per view
|
||||||
|
const responsiveItemsPerView = useMemo(() => {
|
||||||
|
if (windowWidth < 640) return 2; // sm: 2 items
|
||||||
|
if (windowWidth < 768) return 3; // md: 3 items
|
||||||
|
if (windowWidth < 1024) return 4; // lg: 4 items
|
||||||
|
return itemsPerView; // xl: 6 items
|
||||||
|
}, [windowWidth, itemsPerView]);
|
||||||
|
|
||||||
|
// Calculate max index (how many slides we can have)
|
||||||
|
const maxIndex = Math.max(0, partners.length - responsiveItemsPerView);
|
||||||
|
const canNavigate = partners.length > responsiveItemsPerView;
|
||||||
|
const allItemsFit = partners.length <= responsiveItemsPerView;
|
||||||
|
|
||||||
|
// Auto-slide functionality
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canNavigate) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentIndex((prev) => {
|
||||||
|
if (prev >= maxIndex) {
|
||||||
|
return 0; // Loop back to start
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, autoSlideInterval);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [canNavigate, maxIndex, autoSlideInterval]);
|
||||||
|
|
||||||
|
// Reset index when it exceeds max
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentIndex > maxIndex) {
|
||||||
|
setCurrentIndex(0);
|
||||||
|
}
|
||||||
|
}, [maxIndex, currentIndex]);
|
||||||
|
|
||||||
|
const goToPrevious = () => {
|
||||||
|
if (!canNavigate) return;
|
||||||
|
setCurrentIndex((prev) => (prev === 0 ? maxIndex : prev - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNext = () => {
|
||||||
|
if (!canNavigate) return;
|
||||||
|
setCurrentIndex((prev) => (prev >= maxIndex ? 0 : prev + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToSlide = (index: number) => {
|
||||||
|
if (index < 0 || index > maxIndex) return;
|
||||||
|
setCurrentIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (partners.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemWidth = 100 / responsiveItemsPerView;
|
||||||
|
const translateX = allItemsFit ? 0 : -(currentIndex * itemWidth);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-7xl mx-auto">
|
||||||
|
{/* Carousel Container */}
|
||||||
|
<div className={`relative ${allItemsFit ? '' : 'overflow-hidden'} px-4 sm:px-6 lg:px-8`}>
|
||||||
|
{allItemsFit ? (
|
||||||
|
// Centered layout when all items fit - show only logos without containers
|
||||||
|
<div className="flex justify-center items-center flex-wrap gap-4 md:gap-6 lg:gap-8">
|
||||||
|
{partners.map((partner, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`partner-${index}`}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
{partner.link ? (
|
||||||
|
<a
|
||||||
|
href={partner.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center group"
|
||||||
|
>
|
||||||
|
{partner.logo ? (
|
||||||
|
<img
|
||||||
|
src={partner.logo}
|
||||||
|
alt={partner.name || `Partner ${index + 1}`}
|
||||||
|
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
|
||||||
|
{partner.name || `Partner ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{partner.logo ? (
|
||||||
|
<img
|
||||||
|
src={partner.logo}
|
||||||
|
alt={partner.name || `Partner ${index + 1}`}
|
||||||
|
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 hover:opacity-100 transition-opacity duration-300"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
|
||||||
|
{partner.name || `Partner ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Carousel layout when items don't all fit
|
||||||
|
<div
|
||||||
|
className="flex transition-transform duration-500 ease-in-out"
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${translateX}%)`,
|
||||||
|
willChange: 'transform',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{partners.map((partner, index) => (
|
||||||
|
<div
|
||||||
|
key={`partner-${index}`}
|
||||||
|
className="flex-shrink-0 px-2 sm:px-3 md:px-4 flex items-center justify-center"
|
||||||
|
style={{ width: `${itemWidth}%` }}
|
||||||
|
>
|
||||||
|
{partner.link ? (
|
||||||
|
<a
|
||||||
|
href={partner.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center group"
|
||||||
|
>
|
||||||
|
{partner.logo ? (
|
||||||
|
<img
|
||||||
|
src={partner.logo}
|
||||||
|
alt={partner.name || `Partner ${index + 1}`}
|
||||||
|
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
|
||||||
|
{partner.name || `Partner ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{partner.logo ? (
|
||||||
|
<img
|
||||||
|
src={partner.logo}
|
||||||
|
alt={partner.name || `Partner ${index + 1}`}
|
||||||
|
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 hover:opacity-100 transition-opacity duration-300"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
|
||||||
|
{partner.name || `Partner ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
{showNavigation && canNavigate && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={goToPrevious}
|
||||||
|
disabled={currentIndex === 0}
|
||||||
|
className="absolute left-0 top-1/2 -translate-y-1/2 z-10
|
||||||
|
w-10 h-10 sm:w-12 sm:h-12
|
||||||
|
bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||||
|
text-white rounded-full
|
||||||
|
flex items-center justify-center
|
||||||
|
shadow-lg shadow-[#d4af37]/40
|
||||||
|
hover:shadow-xl hover:shadow-[#d4af37]/50
|
||||||
|
hover:scale-110
|
||||||
|
active:scale-95
|
||||||
|
transition-all duration-300
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||||
|
group backdrop-blur-sm"
|
||||||
|
aria-label="Previous partners"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5 sm:w-6 sm:h-6 group-hover:-translate-x-0.5 transition-transform" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={goToNext}
|
||||||
|
disabled={currentIndex >= maxIndex}
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 z-10
|
||||||
|
w-10 h-10 sm:w-12 sm:h-12
|
||||||
|
bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||||
|
text-white rounded-full
|
||||||
|
flex items-center justify-center
|
||||||
|
shadow-lg shadow-[#d4af37]/40
|
||||||
|
hover:shadow-xl hover:shadow-[#d4af37]/50
|
||||||
|
hover:scale-110
|
||||||
|
active:scale-95
|
||||||
|
transition-all duration-300
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||||
|
group backdrop-blur-sm"
|
||||||
|
aria-label="Next partners"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5 sm:w-6 sm:h-6 group-hover:translate-x-0.5 transition-transform" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dots Indicator */}
|
||||||
|
{canNavigate && maxIndex > 0 && (
|
||||||
|
<div className="flex justify-center items-center gap-1.5 sm:gap-2 mt-6 md:mt-8">
|
||||||
|
{Array.from({ length: maxIndex + 1 }).map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => goToSlide(index)}
|
||||||
|
className={`transition-all duration-300 rounded-full
|
||||||
|
${
|
||||||
|
index === currentIndex
|
||||||
|
? 'w-8 h-2 sm:w-10 sm:h-2.5 bg-gradient-to-r from-[#d4af37] to-[#c9a227] shadow-lg shadow-[#d4af37]/40'
|
||||||
|
: 'w-2 h-2 sm:w-2.5 sm:h-2.5 bg-gray-300 hover:bg-[#d4af37]/50'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
aria-label={`Go to partners slide ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PartnersCarousel;
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
RoomCarousel,
|
RoomCarousel,
|
||||||
SearchRoomForm,
|
SearchRoomForm,
|
||||||
} from '../../rooms/components';
|
} from '../../rooms/components';
|
||||||
|
import PartnersCarousel from '../components/PartnersCarousel';
|
||||||
import bannerService from '../services/bannerService';
|
import bannerService from '../services/bannerService';
|
||||||
import roomService from '../../rooms/services/roomService';
|
import roomService from '../../rooms/services/roomService';
|
||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
@@ -1194,7 +1195,8 @@ const HomePage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
{pageContent?.partners && pageContent.partners.length > 0 && (
|
{pageContent?.partners && pageContent.partners.length > 0 && (
|
||||||
<section className="container mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16 bg-gradient-to-b from-white via-gray-50/20 to-white">
|
<section className="w-full py-12 md:py-16 bg-gradient-to-b from-white via-gray-50/20 to-white">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center mb-8 md:mb-10 animate-fade-in">
|
<div className="text-center mb-8 md:mb-10 animate-fade-in">
|
||||||
<div className="inline-block mb-3">
|
<div className="inline-block mb-3">
|
||||||
<div className="h-0.5 w-16 md:w-20 bg-gradient-to-r from-[#d4af37] to-[#f5d76e] mx-auto rounded-full"></div>
|
<div className="h-0.5 w-16 md:w-20 bg-gradient-to-r from-[#d4af37] to-[#f5d76e] mx-auto rounded-full"></div>
|
||||||
@@ -1208,29 +1210,13 @@ const HomePage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6 md:gap-8 px-4">
|
|
||||||
{pageContent.partners.map((partner: any, index: number) => (
|
|
||||||
<div key={index} className="flex items-center justify-center p-4 md:p-6 bg-white rounded-lg md:rounded-xl shadow-md hover:shadow-xl hover:shadow-[#d4af37]/10 transition-all duration-300 border border-gray-100/50 hover:border-[#d4af37]/25 group" style={{ animationDelay: `${index * 0.05}s` }}>
|
|
||||||
{partner.link ? (
|
|
||||||
<a href={partner.link} target="_blank" rel="noopener noreferrer" className="w-full h-full flex items-center justify-center">
|
|
||||||
{partner.logo ? (
|
|
||||||
<img src={partner.logo} alt={partner.name} className="max-w-full max-h-16 md:max-h-20 object-contain opacity-70 group-hover:opacity-100 transition-opacity duration-300" />
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-600 font-semibold text-sm md:text-base">{partner.name}</span>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{partner.logo ? (
|
|
||||||
<img src={partner.logo} alt={partner.name} className="max-w-full max-h-16 md:max-h-20 object-contain opacity-70 group-hover:opacity-100 transition-opacity duration-300" />
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-600 font-semibold text-sm md:text-base">{partner.name}</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<PartnersCarousel
|
||||||
|
partners={pageContent.partners}
|
||||||
|
autoSlideInterval={5000}
|
||||||
|
showNavigation={true}
|
||||||
|
itemsPerView={6}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
search: '',
|
search: '',
|
||||||
status: '',
|
status: '',
|
||||||
});
|
});
|
||||||
const [serviceCurrentPage, setServiceCurrentPage] = useState(1);
|
const [serviceCurrentPage, setServiceCurrentPage] = useState<number>(1);
|
||||||
const [serviceTotalPages, setServiceTotalPages] = useState(1);
|
const [serviceTotalPages, setServiceTotalPages] = useState(1);
|
||||||
const [serviceTotalItems, setServiceTotalItems] = useState(0);
|
const [serviceTotalItems, setServiceTotalItems] = useState(0);
|
||||||
const serviceItemsPerPage = 10;
|
const serviceItemsPerPage = 10;
|
||||||
@@ -400,18 +400,144 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
// Remove contact_info for contact and footer pages since it's now managed centrally
|
// Remove contact_info for contact and footer pages since it's now managed centrally
|
||||||
const { contact_info, luxury_services, ...dataToSave } = data;
|
const { contact_info, luxury_services, ...dataToSave } = data;
|
||||||
|
|
||||||
|
// Clean up data: remove undefined values and ensure proper types
|
||||||
|
const cleanData: any = {};
|
||||||
|
|
||||||
|
// List of fields that should be strings (not objects/arrays)
|
||||||
|
const stringFields = [
|
||||||
|
'title', 'subtitle', 'description', 'content', 'meta_title', 'meta_description', 'meta_keywords',
|
||||||
|
'og_title', 'og_description', 'og_image', 'canonical_url', 'hero_title', 'hero_subtitle', 'hero_image',
|
||||||
|
'story_content', 'about_hero_image', 'mission', 'vision',
|
||||||
|
'amenities_section_title', 'amenities_section_subtitle',
|
||||||
|
'testimonials_section_title', 'testimonials_section_subtitle',
|
||||||
|
'gallery_section_title', 'gallery_section_subtitle',
|
||||||
|
'luxury_section_title', 'luxury_section_subtitle', 'luxury_section_image',
|
||||||
|
'luxury_gallery_section_title', 'luxury_gallery_section_subtitle',
|
||||||
|
'luxury_testimonials_section_title', 'luxury_testimonials_section_subtitle',
|
||||||
|
'about_preview_title', 'about_preview_subtitle', 'about_preview_content', 'about_preview_image',
|
||||||
|
'luxury_services_section_title', 'luxury_services_section_subtitle',
|
||||||
|
'luxury_experiences_section_title', 'luxury_experiences_section_subtitle',
|
||||||
|
'awards_section_title', 'awards_section_subtitle',
|
||||||
|
'cta_title', 'cta_subtitle', 'cta_button_text', 'cta_button_link', 'cta_image',
|
||||||
|
'partners_section_title', 'partners_section_subtitle',
|
||||||
|
'copyright_text', 'map_url'
|
||||||
|
];
|
||||||
|
|
||||||
|
Object.keys(dataToSave).forEach((key) => {
|
||||||
|
const value = (dataToSave as any)[key];
|
||||||
|
|
||||||
|
// Skip undefined values
|
||||||
|
if (value === undefined) return;
|
||||||
|
|
||||||
|
// Handle string fields - ensure they're strings, skip empty/null values
|
||||||
|
if (stringFields.includes(key)) {
|
||||||
|
if (typeof value === 'string' && value.trim() !== '') {
|
||||||
|
cleanData[key] = value.trim();
|
||||||
|
} else {
|
||||||
|
// Skip empty strings and null values - don't send them
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty strings - skip them (don't send empty values)
|
||||||
|
if (value === '' || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle arrays - only send non-empty arrays
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length > 0) {
|
||||||
|
cleanData[key] = value;
|
||||||
|
}
|
||||||
|
// Skip empty arrays - don't send them
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle objects - keep as is (for contact_info, social_links, etc.)
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
cleanData[key] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other types - ensure they're valid
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
cleanData[key] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip any other unexpected types
|
||||||
|
console.warn(`Skipping field ${key} with unexpected type: ${typeof value}`, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log the data being sent for debugging
|
||||||
|
console.log(`Saving ${pageType} page content:`, {
|
||||||
|
fieldCount: Object.keys(cleanData).length,
|
||||||
|
fields: Object.keys(cleanData),
|
||||||
|
sampleData: Object.entries(cleanData).slice(0, 5).reduce((acc, [key, value]) => {
|
||||||
|
acc[key] = Array.isArray(value) ? `Array[${value.length}]` : typeof value === 'object' ? 'Object' : value;
|
||||||
|
return acc;
|
||||||
|
}, {} as any)
|
||||||
|
});
|
||||||
|
|
||||||
if (pageType === 'contact' || pageType === 'footer') {
|
if (pageType === 'contact' || pageType === 'footer') {
|
||||||
await pageContentService.updatePageContent(pageType, dataToSave);
|
await pageContentService.updatePageContent(pageType, cleanData);
|
||||||
} else if (pageType === 'home') {
|
} else if (pageType === 'home') {
|
||||||
// For home page, exclude luxury_services (services are managed in Service Management)
|
// For home page, exclude luxury_services (services are managed in Service Management)
|
||||||
await pageContentService.updatePageContent(pageType, dataToSave);
|
await pageContentService.updatePageContent(pageType, cleanData);
|
||||||
} else {
|
} else {
|
||||||
await pageContentService.updatePageContent(pageType, data);
|
await pageContentService.updatePageContent(pageType, cleanData);
|
||||||
}
|
}
|
||||||
toast.success(`${pageType.charAt(0).toUpperCase() + pageType.slice(1)} content saved successfully`);
|
toast.success(`${pageType.charAt(0).toUpperCase() + pageType.slice(1)} content saved successfully`);
|
||||||
await fetchAllPageContents();
|
await fetchAllPageContents();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.response?.data?.message || `Failed to save ${pageType} content`);
|
console.error('Error saving page content:', error);
|
||||||
|
|
||||||
|
// Log detailed validation errors
|
||||||
|
if (error.response?.data?.errors) {
|
||||||
|
console.error('Full validation errors:', JSON.stringify(error.response.data.errors, null, 2));
|
||||||
|
const fieldErrors = error.response.data.errors.map((err: any, index: number) => {
|
||||||
|
const field = Array.isArray(err.loc) ? err.loc.join('.') : (err.loc || err.field || `error_${index}`);
|
||||||
|
const msg = err.msg || err.message || 'Unknown error';
|
||||||
|
const type = err.type || 'validation_error';
|
||||||
|
return { field, msg, type, full: err };
|
||||||
|
});
|
||||||
|
console.error('Parsed field errors:', fieldErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get error message - check multiple possible error formats
|
||||||
|
let errorMessage = error.response?.data?.detail || error.response?.data?.message;
|
||||||
|
|
||||||
|
// Format validation errors for display - handle different error formats
|
||||||
|
if (error.response?.data?.errors && Array.isArray(error.response.data.errors) && error.response.data.errors.length > 0) {
|
||||||
|
const errorList = error.response.data.errors.map((err: any, index: number) => {
|
||||||
|
const field = Array.isArray(err.loc) ? err.loc.join('.') : (err.loc || err.field || `Field ${index + 1}`);
|
||||||
|
const msg = err.msg || err.message || 'Invalid value';
|
||||||
|
return `${field}: ${msg}`;
|
||||||
|
}).join('\n');
|
||||||
|
errorMessage = `Validation errors:\n${errorList}`;
|
||||||
|
|
||||||
|
// Also log to console for debugging
|
||||||
|
console.error('Validation error details:', errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle FastAPI validation error format
|
||||||
|
if (error.response?.data?.detail && Array.isArray(error.response.data.detail)) {
|
||||||
|
const errorList = error.response.data.detail.map((err: any, index: number) => {
|
||||||
|
const field = Array.isArray(err.loc) ? err.loc.join('.') : (err.loc || `Field ${index + 1}`);
|
||||||
|
const msg = err.msg || 'Invalid value';
|
||||||
|
return `${field}: ${msg}`;
|
||||||
|
}).join('\n');
|
||||||
|
errorMessage = `Validation errors:\n${errorList}`;
|
||||||
|
console.error('FastAPI validation errors:', errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errorMessage) {
|
||||||
|
errorMessage = `Failed to save ${pageType} content`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1006,8 +1132,8 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||||
placeholder="https://example.com/hero-image.jpg or upload"
|
placeholder="https://example.com/hero-image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-4 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-2">
|
<label className="px-5 py-3.5 bg-gradient-to-r from-purple-600 to-purple-700 text-white rounded-xl font-bold hover:from-purple-700 hover:to-purple-800 transition-all cursor-pointer flex items-center gap-2 shadow-lg hover:shadow-xl border-2 border-purple-500/50 hover:border-purple-400">
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-5 h-5" />
|
||||||
Upload
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -1064,12 +1190,41 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">OG Image URL</label>
|
<label className="block text-sm font-semibold text-gray-700 mb-2">OG Image URL</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={homeData.og_image || ''}
|
value={homeData.og_image || ''}
|
||||||
onChange={(e) => setHomeData({ ...homeData, og_image: e.target.value })}
|
onChange={(e) => setHomeData({ ...homeData, og_image: e.target.value })}
|
||||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||||
|
placeholder="https://example.com/og-image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
|
<label className="px-5 py-3.5 bg-gradient-to-r from-purple-600 to-purple-700 text-white rounded-xl font-bold hover:from-purple-700 hover:to-purple-800 transition-all cursor-pointer flex items-center gap-2 shadow-lg hover:shadow-xl border-2 border-purple-500/50 hover:border-purple-400">
|
||||||
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
||||||
|
setHomeData((prevData) => ({ ...prevData, og_image: imageUrl }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{homeData.og_image && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={homeData.og_image.startsWith('http') ? homeData.og_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${homeData.og_image}`}
|
||||||
|
alt="OG Image preview"
|
||||||
|
className="max-w-full max-h-48 rounded-lg border border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1178,8 +1333,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
||||||
placeholder="URL or upload"
|
placeholder="URL or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-lg font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -1278,8 +1434,8 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
||||||
placeholder="https://example.com/luxury-image.jpg or upload"
|
placeholder="https://example.com/luxury-image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-4 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-2">
|
<label className="px-5 py-3.5 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-xl font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400">
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-5 h-5" />
|
||||||
Upload
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -1457,8 +1613,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
||||||
placeholder="URL or upload"
|
placeholder="URL or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-lg font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -1614,8 +1771,8 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
||||||
placeholder="URL or upload"
|
placeholder="URL or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-4 py-2.5 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-lg font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 text-sm shadow-md hover:shadow-lg border-2 border-amber-500/50 hover:border-amber-400 min-w-[100px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-4 h-4" />
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -1707,8 +1864,8 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
||||||
placeholder="https://example.com/about-image.jpg or upload"
|
placeholder="https://example.com/about-image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-4 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-2">
|
<label className="px-5 py-3.5 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-xl font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400">
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-5 h-5" />
|
||||||
Upload
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -1951,8 +2108,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
||||||
placeholder="URL or upload"
|
placeholder="URL or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-lg font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -2115,8 +2273,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
||||||
placeholder="URL or upload"
|
placeholder="URL or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-lg font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -2235,8 +2394,8 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
||||||
placeholder="https://example.com/cta-image.jpg or upload"
|
placeholder="https://example.com/cta-image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-4 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-2">
|
<label className="px-5 py-3.5 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-xl font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400">
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-5 h-5" />
|
||||||
Upload
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -2322,7 +2481,8 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<Trash2 className="w-5 h-5" />
|
<Trash2 className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Name</label>
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Name</label>
|
||||||
<input
|
<input
|
||||||
@@ -2337,20 +2497,6 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
placeholder="Partner Name"
|
placeholder="Partner Name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Logo URL</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={partner?.logo || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
|
|
||||||
current[index] = { ...current[index], logo: e.target.value };
|
|
||||||
setHomeData((prevData) => ({ ...prevData, partners: current }));
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
||||||
placeholder="https://example.com/logo.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Link (Optional)</label>
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Link (Optional)</label>
|
||||||
<input
|
<input
|
||||||
@@ -2366,6 +2512,54 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-t-2 border-gray-300 pt-8 mt-4">
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-4">Logo</label>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={partner?.logo || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
|
||||||
|
current[index] = { ...current[index], logo: e.target.value };
|
||||||
|
setHomeData((prevData) => ({ ...prevData, partners: current }));
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
||||||
|
placeholder="https://example.com/logo.png or upload below"
|
||||||
|
/>
|
||||||
|
<label className="block w-full px-5 py-4 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-lg font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center justify-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400">
|
||||||
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload Logo Image
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
||||||
|
setHomeData((prevData) => {
|
||||||
|
const current = Array.isArray(prevData.partners) ? [...prevData.partners] : [];
|
||||||
|
current[index] = { ...current[index], logo: imageUrl };
|
||||||
|
return { ...prevData, partners: current };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{partner?.logo && (
|
||||||
|
<div className="mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<p className="text-xs text-gray-600 mb-2 font-semibold">Preview:</p>
|
||||||
|
<img
|
||||||
|
src={partner.logo.startsWith('http') ? partner.logo : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${partner.logo}`}
|
||||||
|
alt="Partner logo preview"
|
||||||
|
className="max-w-full max-h-32 rounded-lg border border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(!homeData.partners || homeData.partners.length === 0) && (
|
{(!homeData.partners || homeData.partners.length === 0) && (
|
||||||
@@ -3248,8 +3442,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
placeholder="https://example.com/image.jpg or upload"
|
placeholder="https://example.com/image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-purple-600 to-purple-700 text-white rounded-lg font-bold hover:from-purple-700 hover:to-purple-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-purple-500/50 hover:border-purple-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -3407,8 +3602,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
placeholder="https://example.com/image.jpg or upload"
|
placeholder="https://example.com/image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-purple-600 to-purple-700 text-white rounded-lg font-bold hover:from-purple-700 hover:to-purple-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-purple-500/50 hover:border-purple-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -3563,8 +3759,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
placeholder="https://example.com/image.jpg or upload"
|
placeholder="https://example.com/image.jpg or upload"
|
||||||
/>
|
/>
|
||||||
<label className="px-3 py-2 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
<label className="px-5 py-3 bg-gradient-to-r from-purple-600 to-purple-700 text-white rounded-lg font-bold hover:from-purple-700 hover:to-purple-800 transition-all cursor-pointer flex items-center gap-2 text-base shadow-lg hover:shadow-xl border-2 border-purple-500/50 hover:border-purple-400 min-w-[120px] justify-center">
|
||||||
<Upload className="w-3 h-3" />
|
<Upload className="w-5 h-5" />
|
||||||
|
Upload
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -4785,9 +4982,9 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="service-image-upload"
|
htmlFor="service-image-upload"
|
||||||
className="px-4 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors cursor-pointer flex items-center gap-2 font-medium"
|
className="px-5 py-3.5 bg-gradient-to-r from-amber-600 to-amber-700 text-white rounded-xl font-bold hover:from-amber-700 hover:to-amber-800 transition-all cursor-pointer flex items-center gap-2 shadow-lg hover:shadow-xl border-2 border-amber-500/50 hover:border-amber-400"
|
||||||
>
|
>
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-5 h-5" />
|
||||||
Upload
|
Upload
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user