diff --git a/frontend/admin.js b/frontend/admin.js
index bdd997c..51fa1de 100644
--- a/frontend/admin.js
+++ b/frontend/admin.js
@@ -52,8 +52,7 @@ 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 extraPhotoInput = document.querySelector("#extraPhotoInput");
+const photoUploadZone = document.querySelector("#photoUploadZone");
const extraPhotoGallery = document.querySelector("#extraPhotoGallery");
const tableBody = document.querySelector("#adminTable tbody");
@@ -324,7 +323,6 @@ 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" });
}
@@ -342,7 +340,6 @@ resetBtn.addEventListener("click", () => {
vehicleForm.price_per_km_eur.value = 1.50;
state.currentPhotoPath = null;
state.vehiclePhotos = [];
- updatePreview("");
formTitle.textContent = "Neues Fahrzeug";
formFeedback.textContent = "";
});
@@ -408,36 +405,8 @@ async function deleteVehicle(id) {
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);
- }
-});
-function updatePreview(url) { photoPreview.style.backgroundImage = url ? `url('${url}')` : ""; }
+// ----- Unified Photo Upload + Gallery -----
-// ----- Vehicle Photo Gallery -----
async function loadVehiclePhotos(vehicleId) {
if (!vehicleId) {
state.vehiclePhotos = [];
@@ -457,33 +426,136 @@ async function loadVehiclePhotos(vehicleId) {
function renderExtraPhotoGallery() {
if (!extraPhotoGallery) return;
extraPhotoGallery.innerHTML = "";
- for (const ph of state.vehiclePhotos) {
- const wrapper = document.createElement("div");
- wrapper.style.cssText = "position:relative;border-radius:8px;overflow:hidden;aspect-ratio:16/10;background:#1a1a1a;";
- wrapper.innerHTML = `
-
- ${!ph.is_primary ? `
` : ''}
-
+ 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 ? '
Hauptfoto' : ''}
+
+ ${!ph.is_primary ? `` : ''}
+
+
+ ${ph.is_primary ? '
Hauptfoto' : ''}
+
â ¿
`;
- extraPhotoGallery.appendChild(wrapper);
+ extraPhotoGallery.appendChild(card);
}
- // Event listeners
- extraPhotoGallery.querySelectorAll("[data-delete-photo]").forEach(btn => {
+ // Action buttons
+ extraPhotoGallery.querySelectorAll(".admin-photo-delete").forEach(btn => {
btn.addEventListener("click", async () => {
- const phId = btn.dataset.deletePhoto;
- await deleteVehiclePhoto(phId);
+ await deleteVehiclePhoto(btn.dataset.photoId);
});
});
- extraPhotoGallery.querySelectorAll("[data-set-primary]").forEach(btn => {
+ extraPhotoGallery.querySelectorAll(".admin-photo-set-primary").forEach(btn => {
btn.addEventListener("click", async () => {
- const phId = btn.dataset.setPrimary;
- await setPrimaryPhoto(phId);
+ 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) {
@@ -496,6 +568,16 @@ async function deleteVehiclePhoto(photoId) {
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);
@@ -506,25 +588,28 @@ 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);
}
}
-// Extra photos upload
-extraPhotoInput?.addEventListener("change", async () => {
- const files = extraPhotoInput.files;
+// 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 Fahrzeug speichern, dann Fotos hinzufügen.";
+ formFeedback.textContent = "Bitte zuerst das Fahrzeug speichern, dann Fotos hinzufügen.";
return;
}
formFeedback.className = "form-feedback";
- formFeedback.textContent = "Uploading photos...";
+ formFeedback.textContent = `Uploading ${files.length} photo(s)...`;
+ let uploaded = 0;
for (const file of files) {
try {
const ext = (file.name.split(".").pop() || "jpg").toLowerCase();
@@ -534,23 +619,49 @@ extraPhotoInput?.addEventListener("change", async () => {
.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: state.vehiclePhotos.length === 0,
+ is_primary: isFirst,
});
+ if (isFirst) {
+ vehicleForm.photo_url.value = pub.publicUrl;
+ }
+ uploaded++;
} catch (err) {
console.error("Upload failed:", err);
}
}
await loadVehiclePhotos(vid);
- formFeedback.textContent = `${files.length} Foto(s) hochgeladen.`;
- extraPhotoInput.value = "";
+ formFeedback.textContent = `${uploaded} Foto(s) hochgeladen.`;
+ photoInput.value = "";
});
+// 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/styles.css b/frontend/styles.css
index 18b1ddb..3a15b9e 100644
--- a/frontend/styles.css
+++ b/frontend/styles.css
@@ -1135,6 +1135,172 @@ table.admin-table td:last-child { white-space: nowrap; }
filter: brightness(1.1);
}
+/* ---- Unified Photo Upload Zone ---- */
+.admin-photo-upload-zone {
+ width: 100%;
+ min-height: 120px;
+ border: 2px dashed var(--line);
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: border-color 0.2s ease, background 0.2s ease;
+ background: var(--bg-elev);
+ margin-bottom: 0.5rem;
+ position: relative;
+}
+.admin-photo-upload-zone:hover {
+ border-color: var(--accent-strong);
+}
+.admin-photo-upload-zone.drag-active {
+ border-color: var(--accent-strong);
+ background: rgba(245, 158, 11, 0.08);
+}
+.admin-photo-upload-zone input[type="file"] {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ cursor: pointer;
+}
+.admin-photo-upload-content {
+ text-align: center;
+ pointer-events: none;
+}
+.admin-photo-upload-icon {
+ font-size: 2rem;
+ display: block;
+ margin-bottom: 0.3rem;
+}
+
+/* ---- Photo Gallery ---- */
+.admin-photo-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 0.8rem;
+ margin-top: 1rem;
+}
+
+.admin-photo-card {
+ position: relative;
+ border-radius: 10px;
+ overflow: hidden;
+ aspect-ratio: 16/10;
+ background: #1a1a1a;
+ border: 2px solid transparent;
+ transition: border-color 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
+ cursor: grab;
+}
+.admin-photo-card:hover {
+ border-color: var(--accent-strong);
+ transform: scale(1.02);
+}
+.admin-photo-card:active {
+ cursor: grabbing;
+}
+.admin-photo-card-drag-over {
+ border-color: #f59e0b !important;
+ transform: scale(1.05);
+}
+.admin-photo-card img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ pointer-events: none;
+}
+.admin-photo-card-arrows {
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ display: flex;
+ gap: 3px;
+ z-index: 2;
+}
+.admin-photo-arrow {
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ border: none;
+ background: rgba(0, 0, 0, 0.7);
+ color: #fff;
+ font-size: 1rem;
+ font-weight: 700;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s ease;
+}
+.admin-photo-arrow:hover {
+ background: rgba(0, 0, 0, 0.9);
+}
+.admin-photo-card-actions {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ display: flex;
+ gap: 3px;
+ z-index: 2;
+}
+.admin-photo-set-primary {
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ border: none;
+ background: rgba(245, 158, 11, 0.85);
+ color: #000;
+ font-size: 1rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s ease;
+}
+.admin-photo-set-primary:hover {
+ background: #f59e0b;
+}
+.admin-photo-delete {
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ border: none;
+ background: rgba(239, 68, 68, 0.85);
+ color: #fff;
+ font-size: 1rem;
+ font-weight: 700;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s ease;
+}
+.admin-photo-delete:hover {
+ background: #ef4444;
+}
+.admin-photo-badge {
+ position: absolute;
+ bottom: 6px;
+ left: 6px;
+ background: #22c55e;
+ color: #fff;
+ border-radius: 5px;
+ padding: 2px 8px;
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ z-index: 2;
+}
+.admin-photo-drag-handle {
+ position: absolute;
+ bottom: 6px;
+ right: 6px;
+ color: rgba(255, 255, 255, 0.5);
+ font-size: 1.1rem;
+ z-index: 2;
+ pointer-events: none;
+}
+
/* ---------------- Forms / Toggle Switch ---------------- */
.toggle-switch {
position: relative;