diff --git a/frontend/admin.js b/frontend/admin.js
index 5cfb203..51fa1de 100644
--- a/frontend/admin.js
+++ b/frontend/admin.js
@@ -52,7 +52,8 @@ const formTitle = document.querySelector("#formTitle");
const saveBtn = document.querySelector("#saveBtn");
const resetBtn = document.querySelector("#resetBtn");
const photoInput = document.querySelector("#photoInput");
-const photoPreview = document.querySelector("#photoPreview");
+const photoUploadZone = document.querySelector("#photoUploadZone");
+const extraPhotoGallery = document.querySelector("#extraPhotoGallery");
const tableBody = document.querySelector("#adminTable tbody");
// ----- State -----
@@ -66,6 +67,7 @@ const state = {
vehicles: [],
vehicleMap: new Map(),
currentPhotoPath: null,
+ vehiclePhotos: [],
realtimeChannel: null,
forcedRotation: false,
};
@@ -321,7 +323,7 @@ function loadForEdit(id) {
vehicleForm.photo_url.value = v.photo_url;
vehicleForm.is_active.checked = v.is_active;
state.currentPhotoPath = v.photo_path || null;
- updatePreview(v.photo_url);
+ loadVehiclePhotos(v.id);
window.scrollTo({ top: 0, behavior: "smooth" });
}
@@ -337,7 +339,7 @@ resetBtn.addEventListener("click", () => {
vehicleForm.kaution_eur.value = 5000;
vehicleForm.price_per_km_eur.value = 1.50;
state.currentPhotoPath = null;
- updatePreview("");
+ state.vehiclePhotos = [];
formTitle.textContent = "Neues Fahrzeug";
formFeedback.textContent = "";
});
@@ -390,41 +392,275 @@ async function deleteVehicle(id) {
const v = state.vehicleMap.get(id);
if (!v) return;
if (!confirm(`Delete ${v.brand} ${v.model}?`)) return;
+ // Delete old main photo
if (v.photo_path) await supabase.storage.from("vehicle-photos").remove([v.photo_path]);
+ // Delete gallery photos from storage
+ const { data: photos } = await supabase.from("vehicle_photos").select("photo_path").eq("vehicle_id", id);
+ if (photos?.length) {
+ await supabase.storage.from("vehicle-photos").remove(photos.map(p => p.photo_path));
+ }
const { error } = await supabase.from("vehicles").delete().eq("id", id);
if (error) { alert(error.message); return; }
await loadVehicles();
renderVehicles();
}
-// Photo upload
-photoInput.addEventListener("change", async () => {
- const file = photoInput.files?.[0];
- if (!file) return;
- formFeedback.className = "form-feedback";
- formFeedback.textContent = "Uploading photo...";
- try {
- // Delete old photo if exists
- if (state.currentPhotoPath) {
- await supabase.storage.from("vehicle-photos").remove([state.currentPhotoPath]);
- }
- const ext = (file.name.split(".").pop() || "jpg").toLowerCase();
- const path = `${crypto.randomUUID()}.${ext}`;
- const { error: upErr } = await supabase.storage
- .from("vehicle-photos")
- .upload(path, file, { contentType: file.type, upsert: true });
- if (upErr) throw upErr;
- const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path);
- state.currentPhotoPath = path;
- vehicleForm.photo_url.value = pub.publicUrl;
- updatePreview(pub.publicUrl);
- formFeedback.textContent = "Upload ok.";
- } catch (err) {
- formFeedback.className = "form-feedback error";
- formFeedback.textContent = err.message || String(err);
+// ----- Unified Photo Upload + Gallery -----
+
+async function loadVehiclePhotos(vehicleId) {
+ if (!vehicleId) {
+ state.vehiclePhotos = [];
+ renderExtraPhotoGallery();
+ return;
}
+ const { data, error } = await supabase
+ .from("vehicle_photos")
+ .select("*")
+ .eq("vehicle_id", vehicleId)
+ .order("display_order", { ascending: true });
+ if (error) { console.error("Failed to load vehicle photos:", error); return; }
+ state.vehiclePhotos = data || [];
+ renderExtraPhotoGallery();
+}
+
+function renderExtraPhotoGallery() {
+ if (!extraPhotoGallery) return;
+ extraPhotoGallery.innerHTML = "";
+ const photos = state.vehiclePhotos;
+ if (!photos.length) return;
+
+ for (let i = 0; i < photos.length; i++) {
+ const ph = photos[i];
+ const card = document.createElement("div");
+ card.className = "admin-photo-card";
+ card.draggable = true;
+ card.dataset.photoId = ph.id;
+ card.dataset.photoIdx = i;
+ card.innerHTML = `
+
})
+
+
+
+
+
+ ${!ph.is_primary ? `` : ''}
+
+
+ ${ph.is_primary ? '
Hauptfoto' : ''}
+
â ż
+ `;
+ extraPhotoGallery.appendChild(card);
+ }
+
+ // Action buttons
+ extraPhotoGallery.querySelectorAll(".admin-photo-delete").forEach(btn => {
+ btn.addEventListener("click", async () => {
+ await deleteVehiclePhoto(btn.dataset.photoId);
+ });
+ });
+ extraPhotoGallery.querySelectorAll(".admin-photo-set-primary").forEach(btn => {
+ btn.addEventListener("click", async () => {
+ await setPrimaryPhoto(btn.dataset.photoId);
+ });
+ });
+ extraPhotoGallery.querySelectorAll(".admin-photo-arrow").forEach(btn => {
+ btn.addEventListener("click", async () => {
+ const card = btn.closest(".admin-photo-card");
+ const idx = +card.dataset.photoIdx;
+ const dir = +btn.dataset.moveDir;
+ await reorderPhoto(idx, dir);
+ });
+ });
+
+ // Drag and drop
+ extraPhotoGallery.querySelectorAll(".admin-photo-card").forEach(card => {
+ card.addEventListener("dragstart", handleDragStart);
+ card.addEventListener("dragover", handleDragOver);
+ card.addEventListener("dragenter", handleDragEnter);
+ card.addEventListener("dragleave", handleDragLeave);
+ card.addEventListener("drop", handleDrop);
+ card.addEventListener("dragend", handleDragEnd);
+ });
+}
+
+let draggedPhotoIdx = null;
+
+function handleDragStart(e) {
+ draggedPhotoIdx = +this.dataset.photoIdx;
+ e.dataTransfer.effectAllowed = "move";
+ e.dataTransfer.setData("text/plain", this.dataset.photoId);
+ this.style.opacity = "0.4";
+}
+
+function handleDragOver(e) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+}
+
+function handleDragEnter(e) {
+ e.preventDefault();
+ if (+this.dataset.photoIdx !== draggedPhotoIdx) {
+ this.classList.add("admin-photo-card-drag-over");
+ }
+}
+
+function handleDragLeave() {
+ this.classList.remove("admin-photo-card-drag-over");
+}
+
+function handleDrop(e) {
+ e.preventDefault();
+ this.classList.remove("admin-photo-card-drag-over");
+ const targetIdx = +this.dataset.photoIdx;
+ if (draggedPhotoIdx !== null && draggedPhotoIdx !== targetIdx) {
+ const dir = targetIdx > draggedPhotoIdx ? 1 : -1;
+ reorderPhoto(draggedPhotoIdx, dir, targetIdx);
+ }
+}
+
+function handleDragEnd() {
+ this.style.opacity = "1";
+ draggedPhotoIdx = null;
+ extraPhotoGallery?.querySelectorAll(".admin-photo-card").forEach(c => c.classList.remove("admin-photo-card-drag-over"));
+}
+
+async function reorderPhoto(fromIdx, dir, targetIdx) {
+ const photos = state.vehiclePhotos;
+ if (photos.length < 2) return;
+
+ let toIdx;
+ if (targetIdx !== undefined) {
+ toIdx = targetIdx;
+ } else {
+ toIdx = fromIdx + dir;
+ if (toIdx < 0 || toIdx >= photos.length) return;
+ }
+
+ // Swap in local array
+ [photos[fromIdx], photos[toIdx]] = [photos[toIdx], photos[fromIdx]];
+
+ // Build order payload
+ const orderPayload = photos.map((p, i) => ({ id: p.id, order: i }));
+
+ const vid = vehicleForm.vid?.value;
+ if (vid) {
+ try {
+ await supabase.rpc("reorder_vehicle_photos", {
+ p_vehicle_id: vid,
+ p_photo_orders: orderPayload,
+ });
+ } catch (err) {
+ console.error("Reorder failed:", err);
+ [photos[fromIdx], photos[toIdx]] = [photos[fromIdx], photos[toIdx]];
+ return;
+ }
+ }
+ renderExtraPhotoGallery();
+}
+
+async function deleteVehiclePhoto(photoId) {
+ const ph = state.vehiclePhotos.find(p => p.id === photoId);
+ if (!ph) return;
+ try {
+ if (ph.photo_path) {
+ await supabase.storage.from("vehicle-photos").remove([ph.photo_path]);
+ }
+ const { error } = await supabase.from("vehicle_photos").delete().eq("id", photoId);
+ if (error) throw error;
+ state.vehiclePhotos = state.vehiclePhotos.filter(p => p.id !== photoId);
+
+ // If deleted was primary, promote next photo
+ if (ph.is_primary && state.vehiclePhotos.length) {
+ const newPrimary = state.vehiclePhotos[0];
+ const vid = vehicleForm.vid?.value;
+ if (vid) {
+ await supabase.rpc("set_primary_vehicle_photo", { p_vehicle_id: vid, p_photo_id: newPrimary.id });
+ vehicleForm.photo_url.value = newPrimary.photo_url;
+ }
+ }
+ renderExtraPhotoGallery();
+ } catch (err) {
+ console.error("Failed to delete photo:", err);
+ }
+}
+
+async function setPrimaryPhoto(photoId) {
+ const vid = vehicleForm.vid?.value;
+ if (!vid) return;
+ try {
+ const ph = state.vehiclePhotos.find(p => p.id === photoId);
+ await supabase.rpc("set_primary_vehicle_photo", { p_vehicle_id: vid, p_photo_id: photoId });
+ vehicleForm.photo_url.value = ph?.photo_url || "";
+ await loadVehiclePhotos(vid);
+ } catch (err) {
+ console.error("Failed to set primary photo:", err);
+ }
+}
+
+// Unified photo upload handler
+photoInput.addEventListener("change", async () => {
+ const files = photoInput.files;
+ if (!files.length) return;
+ const vid = vehicleForm.vid?.value;
+ if (!vid) {
+ formFeedback.className = "form-feedback error";
+ formFeedback.textContent = "Bitte zuerst das Fahrzeug speichern, dann Fotos hinzufĂŒgen.";
+ return;
+ }
+ formFeedback.className = "form-feedback";
+ formFeedback.textContent = `Uploading ${files.length} photo(s)...`;
+ let uploaded = 0;
+ for (const file of files) {
+ try {
+ const ext = (file.name.split(".").pop() || "jpg").toLowerCase();
+ const path = `${vid}/${crypto.randomUUID()}.${ext}`;
+ const { error: upErr } = await supabase.storage
+ .from("vehicle-photos")
+ .upload(path, file, { contentType: file.type, upsert: true });
+ if (upErr) throw upErr;
+ const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path);
+ const isFirst = state.vehiclePhotos.length === 0;
+ const maxOrder = state.vehiclePhotos.reduce((m, p) => Math.max(m, p.display_order), -1);
+ await supabase.from("vehicle_photos").insert({
+ vehicle_id: vid,
+ photo_url: pub.publicUrl,
+ photo_path: path,
+ display_order: maxOrder + 1,
+ is_primary: isFirst,
+ });
+ if (isFirst) {
+ vehicleForm.photo_url.value = pub.publicUrl;
+ }
+ uploaded++;
+ } catch (err) {
+ console.error("Upload failed:", err);
+ }
+ }
+ await loadVehiclePhotos(vid);
+ formFeedback.textContent = `${uploaded} Foto(s) hochgeladen.`;
+ photoInput.value = "";
});
-function updatePreview(url) { photoPreview.style.backgroundImage = url ? `url('${url}')` : ""; }
+
+// Drag-and-drop on upload zone
+if (photoUploadZone) {
+ photoUploadZone.addEventListener("dragover", e => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "copy";
+ photoUploadZone.classList.add("drag-active");
+ });
+ photoUploadZone.addEventListener("dragleave", () => {
+ photoUploadZone.classList.remove("drag-active");
+ });
+ photoUploadZone.addEventListener("drop", e => {
+ e.preventDefault();
+ photoUploadZone.classList.remove("drag-active");
+ const files = e.dataTransfer.files;
+ if (files.length) {
+ photoInput.files = files;
+ photoInput.dispatchEvent(new Event("change"));
+ }
+ });
+}
// =========================================================================
// LEADS
diff --git a/frontend/agb.html b/frontend/agb.html
index fd2fcd2..7631d47 100644
--- a/frontend/agb.html
+++ b/frontend/agb.html
@@ -4,8 +4,8 @@
AGB · MC Cars
-
-
+
+
@@ -51,13 +51,12 @@
@@ -88,16 +87,15 @@
diff --git a/frontend/app.js b/frontend/app.js
index 9d0cd6c..0714ac0 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -16,6 +16,7 @@ const state = {
sort: "sort_order",
maxPrice: null,
reviewIdx: 0,
+ vehiclePhotosMap: new Map(),
};
// ---------------- Elements ----------------
@@ -124,6 +125,31 @@ async function loadVehicles() {
state.vehicles = data || [];
statCarsCount.textContent = state.vehicles.length;
+ // Load vehicle photos
+ if (state.vehicles.length > 0) {
+ const ids = state.vehicles.map(v => v.id);
+ const { data: photos } = await supabase
+ .from("vehicle_photos")
+ .select("*")
+ .in("vehicle_id", ids)
+ .order("display_order", { ascending: true });
+ state.vehiclePhotosMap = new Map();
+ if (photos) {
+ for (const ph of photos) {
+ if (!state.vehiclePhotosMap.has(ph.vehicle_id)) {
+ state.vehiclePhotosMap.set(ph.vehicle_id, []);
+ }
+ state.vehiclePhotosMap.get(ph.vehicle_id).push(ph);
+ }
+ }
+ // Also include legacy main photo if no gallery photos exist
+ for (const v of state.vehicles) {
+ if (!state.vehiclePhotosMap.has(v.id) && v.photo_url) {
+ state.vehiclePhotosMap.set(v.id, [{ photo_url: v.photo_url }]);
+ }
+ }
+ }
+
const brands = [...new Set(state.vehicles.map(v => v.brand))].sort();
brandFilter.innerHTML = `