feat: Add inspiration photo upload functionality to booking system
- Extend booking schema with optional inspirationPhoto field (Base64 encoded) - Implement photo upload in booking form with file validation (max 5MB, image files only) - Add photo preview with remove functionality in booking form - Create thumbnail display in admin bookings management - Implement photo popup modal for full-size image viewing - Add inspiration photo column to bookings table - Include photo upload in form reset after successful booking - Add user-friendly photo upload UI with drag-and-drop styling Features: - Optional photo upload for customer inspiration/reference - File size validation (5MB limit) - File type validation (images only) - Photo preview in booking form - Thumbnail display in admin panel - Full-size popup modal for detailed viewing - Responsive design with hover effects - German localization throughout Changes: - booking-form.tsx: Add photo upload UI and functionality - admin-bookings.tsx: Add photo thumbnails and popup modal - bookings.ts: Extend schema with inspirationPhoto field
This commit is contained in:
@@ -11,6 +11,8 @@ export function BookingForm() {
|
||||
const [selectedSlotId, setSelectedSlotId] = useState<string>("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [agbAccepted, setAgbAccepted] = useState(false);
|
||||
const [inspirationPhoto, setInspirationPhoto] = useState<string>("");
|
||||
const [photoPreview, setPhotoPreview] = useState<string>("");
|
||||
|
||||
const { data: treatments } = useQuery(
|
||||
queryClient.treatments.live.list.experimental_liveOptions()
|
||||
@@ -33,6 +35,39 @@ export function BookingForm() {
|
||||
const selectedTreatmentData = treatments?.find((t) => t.id === selectedTreatment);
|
||||
const availableSlots = (slotsByDate || []).filter((s) => s.status === "free");
|
||||
|
||||
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Check file size (max 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert("Das Foto ist zu groß. Bitte wähle ein Bild unter 5MB.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert("Bitte wähle nur Bilddateien aus.");
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result as string;
|
||||
setInspirationPhoto(result);
|
||||
setPhotoPreview(result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const removePhoto = () => {
|
||||
setInspirationPhoto("");
|
||||
setPhotoPreview("");
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('photo-upload') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedSlotId) {
|
||||
@@ -54,6 +89,7 @@ export function BookingForm() {
|
||||
appointmentDate,
|
||||
appointmentTime,
|
||||
notes,
|
||||
inspirationPhoto,
|
||||
slotId: selectedSlotId,
|
||||
},
|
||||
{
|
||||
@@ -66,6 +102,11 @@ export function BookingForm() {
|
||||
setSelectedSlotId("");
|
||||
setNotes("");
|
||||
setAgbAccepted(false);
|
||||
setInspirationPhoto("");
|
||||
setPhotoPreview("");
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('photo-upload') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen.");
|
||||
},
|
||||
}
|
||||
@@ -202,6 +243,41 @@ export function BookingForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Inspiration Photo Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Inspiration/Beispiel-Foto (optional)
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
id="photo-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoUpload}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-pink-50 file:text-pink-700 hover:file:bg-pink-100"
|
||||
/>
|
||||
{photoPreview && (
|
||||
<div className="relative inline-block">
|
||||
<img
|
||||
src={photoPreview}
|
||||
alt="Inspiration Preview"
|
||||
className="w-32 h-32 object-cover rounded-lg border border-gray-200"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={removePhoto}
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm hover:bg-red-600"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-500">
|
||||
📸 Lade ein Foto hoch, das als Inspiration für deine Nagelbehandlung dienen soll. Max. 5MB, alle Bildformate erlaubt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AGB Acceptance */}
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
|
Reference in New Issue
Block a user