This commit is contained in:
Iliyan Angelov
2025-12-05 01:50:38 +02:00
parent 99da0afecd
commit e1988fe37a
7 changed files with 623 additions and 133 deletions

File diff suppressed because one or more lines are too long

View File

@@ -51,7 +51,7 @@ class PageContentUpdateRequest(BaseModel):
luxury_features: Optional[Union[str, List[Dict[str, Any]]]] = None
luxury_gallery_section_title: Optional[str] = Field(None, max_length=500)
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_subtitle: Optional[str] = Field(None, max_length=1000)
luxury_testimonials: Optional[Union[str, List[Dict[str, Any]]]] = None

View 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;

View File

@@ -16,6 +16,7 @@ import {
RoomCarousel,
SearchRoomForm,
} from '../../rooms/components';
import PartnersCarousel from '../components/PartnersCarousel';
import bannerService from '../services/bannerService';
import roomService from '../../rooms/services/roomService';
import pageContentService from '../services/pageContentService';
@@ -1194,7 +1195,8 @@ const HomePage: React.FC = () => {
{}
{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="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>
@@ -1208,29 +1210,13 @@ const HomePage: React.FC = () => {
</p>
)}
</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>
<PartnersCarousel
partners={pageContent.partners}
autoSlideInterval={5000}
showNavigation={true}
itemsPerView={6}
/>
</section>
)}

View File

@@ -100,7 +100,7 @@ const PageContentDashboard: React.FC = () => {
search: '',
status: '',
});
const [serviceCurrentPage, setServiceCurrentPage] = useState(1);
const [serviceCurrentPage, setServiceCurrentPage] = useState<number>(1);
const [serviceTotalPages, setServiceTotalPages] = useState(1);
const [serviceTotalItems, setServiceTotalItems] = useState(0);
const serviceItemsPerPage = 10;
@@ -400,18 +400,144 @@ const PageContentDashboard: React.FC = () => {
setSaving(true);
// Remove contact_info for contact and footer pages since it's now managed centrally
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') {
await pageContentService.updatePageContent(pageType, dataToSave);
await pageContentService.updatePageContent(pageType, cleanData);
} else if (pageType === 'home') {
// For home page, exclude luxury_services (services are managed in Service Management)
await pageContentService.updatePageContent(pageType, dataToSave);
await pageContentService.updatePageContent(pageType, cleanData);
} else {
await pageContentService.updatePageContent(pageType, data);
await pageContentService.updatePageContent(pageType, cleanData);
}
toast.success(`${pageType.charAt(0).toUpperCase() + pageType.slice(1)} content saved successfully`);
await fetchAllPageContents();
} 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 {
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"
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">
<Upload className="w-4 h-4" />
<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"
@@ -1064,12 +1190,41 @@ const PageContentDashboard: React.FC = () => {
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">OG Image URL</label>
<div className="flex gap-2">
<input
type="url"
value={homeData.og_image || ''}
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>
@@ -1178,8 +1333,9 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -1278,8 +1434,8 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
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">
<Upload className="w-4 h-4" />
<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-5 h-5" />
Upload
<input
type="file"
@@ -1457,8 +1613,9 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -1614,8 +1771,8 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-4 h-4" />
<input
type="file"
accept="image/*"
@@ -1707,8 +1864,8 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
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">
<Upload className="w-4 h-4" />
<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-5 h-5" />
Upload
<input
type="file"
@@ -1951,8 +2108,9 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -2115,8 +2273,9 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -2235,8 +2394,8 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
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">
<Upload className="w-4 h-4" />
<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-5 h-5" />
Upload
<input
type="file"
@@ -2322,7 +2481,8 @@ const PageContentDashboard: React.FC = () => {
<Trash2 className="w-5 h-5" />
</button>
</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>
<label className="block text-sm font-semibold text-gray-700 mb-2">Name</label>
<input
@@ -2337,20 +2497,6 @@ const PageContentDashboard: React.FC = () => {
placeholder="Partner Name"
/>
</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>
<label className="block text-sm font-semibold text-gray-700 mb-2">Link (Optional)</label>
<input
@@ -2366,6 +2512,54 @@ const PageContentDashboard: React.FC = () => {
/>
</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>
))}
{(!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"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -3407,8 +3602,9 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -3563,8 +3759,9 @@ const PageContentDashboard: React.FC = () => {
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
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">
<Upload className="w-3 h-3" />
<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-5 h-5" />
Upload
<input
type="file"
accept="image/*"
@@ -4785,9 +4982,9 @@ const PageContentDashboard: React.FC = () => {
/>
<label
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
</label>
</div>