Files
shopPRO/product-img-upl.php
2026-01-15 13:51:21 +01:00

402 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Edytor Zdjęć Produktowych</title>
<style>
/* --- GŁÓWNE STYLE --- */
body { font-family: sans-serif; padding: 20px; color: #333; }
h2 { margin-bottom: 20px; }
/* --- WRAPPER CROPPERA --- */
.cropper-wrapper {
position: relative;
width: 100%;
height: 500px;
background: #222;
margin-bottom: 15px;
border-radius: 8px;
overflow: hidden;
display: none; /* Ukryty domyślnie */
user-select: none;
touch-action: none;
}
cropper-canvas { width: 100%; height: 100%; }
/* Stała ramka wyboru */
cropper-selection {
outline: 2px solid #e74c3c !important; /* Czerwona ramka */
pointer-events: none;
}
/* --- OVERLAY I UCHWYTY (HANDLES) --- */
.photo-handles {
position: absolute;
inset: 0;
pointer-events: none; /* Kluczowe: przepuszcza kliki do obrazka */
z-index: 20;
}
.photo-outline {
position: absolute;
inset: 6px;
border: 2px dashed rgba(255, 255, 255, 0.6);
border-radius: 6px;
}
.handle {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
border: 2px solid #007bff;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
pointer-events: auto; /* Tylko kropki są klikalne */
transition: transform 0.1s;
}
.handle:active { transform: scale(1.2); }
/* Pozycje uchwytów */
.handle.tl { left: 10px; top: 10px; cursor: nwse-resize; }
.handle.tr { right: 10px; top: 10px; cursor: nesw-resize; }
.handle.bl { left: 10px; bottom: 10px; cursor: nesw-resize; }
.handle.br { right: 10px; bottom: 10px; cursor: nwse-resize; }
/* --- PASEK NARZĘDZI --- */
#toolbar {
display: none;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
}
#toolbar.show { display: flex; }
.btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
font-size: 15px;
border-radius: 6px;
transition: background 0.2s;
}
.btn:hover { background-color: #0056b3; }
.btn:disabled { background-color: #ccc; cursor: not-allowed; }
.chip {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 20px;
font-size: 13px;
background: #f8f9fa;
font-weight: 600;
}
#hint { font-size: 13px; color: #666; margin-left: auto; }
</style>
</head>
<body>
<h2>Przesyłanie zdjęcia produktu</h2>
<div style="margin-bottom: 20px;">
<label class="btn" for="inputImage">📂 Wybierz zdjęcie</label>
<input type="file" id="inputImage" accept="image/*" style="display:none" />
<span id="fileName" style="margin-left: 10px; color:#555;"></span>
</div>
<div class="cropper-wrapper" id="cropperWrapper">
<cropper-canvas id="my-cropper" background>
<cropper-image
id="cropperImage"
initial-center-size="cover"
translatable
scalable
></cropper-image>
<cropper-handle action="move" plain></cropper-handle>
<cropper-selection id="cropperSelection" initial-coverage="0.6"></cropper-selection>
</cropper-canvas>
<div class="photo-handles" id="photoHandles">
<div class="photo-outline"></div>
<div class="handle tl" data-pos="top" title="Zmień rozmiar"></div>
<div class="handle tr" data-pos="top" title="Zmień rozmiar"></div>
<div class="handle bl" data-pos="bottom" title="Zmień rozmiar"></div>
<div class="handle br" data-pos="bottom" title="Zmień rozmiar"></div>
</div>
</div>
<div id="toolbar">
<button type="button" class="btn" id="btnZoomIn">+</button>
<button type="button" class="btn" id="btnZoomOut">-</button>
<button type="button" class="btn" id="btnReset">Reset</button>
<span class="chip" id="formatInfo"></span>
<span id="hint">Mysz/Dotyk: Przesuń | Kółko/Rogi: Zoom</span>
</div>
<button id="btnUpload" class="btn" style="display:none; width: 100%; padding: 12px;">Przytnij i Zapisz</button>
<div id="result" style="margin-top: 20px;"></div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://unpkg.com/cropperjs@2.1.0/dist/cropper.js"></script>
<script>
/**
* KONFIGURACJA
* Wszystkie stałe w jednym miejscu dla łatwej edycji.
*/
const CONFIG = {
dims: {
adminW: 8, // cm
adminH: 12, // cm
outputW: 800, // px
outputH: 1200 // px
},
zoomSensitivity: 0.004,
selectors: {
input: '#inputImage',
fileName: '#fileName',
wrapper: '#cropperWrapper',
uploadBtn: '#btnUpload',
toolbar: '#toolbar',
result: '#result',
cropperCanvas: 'my-cropper',
cropperImage: 'cropperImage',
cropperSelection: 'cropperSelection',
handles: '.handle'
}
};
/**
* STATE
* Przechowywanie stanu aplikacji
*/
const State = {
isResizing: false,
startY: 0,
isInverted: false // Czy używamy dolnych uchwytów (odwrócona logika)
};
/**
* DOM MANAGER
* Cache'owanie elementów jQuery i JS
*/
const DOM = {
$input: $(CONFIG.selectors.input),
$name: $(CONFIG.selectors.fileName),
$wrapper: $(CONFIG.selectors.wrapper),
$uploadBtn: $(CONFIG.selectors.uploadBtn),
$toolbar: $(CONFIG.selectors.toolbar),
$result: $(CONFIG.selectors.result),
elCanvas: document.getElementById(CONFIG.selectors.cropperCanvas),
elImage: document.getElementById(CONFIG.selectors.cropperImage),
elSelection: document.getElementById(CONFIG.selectors.cropperSelection),
elsHandles: document.querySelectorAll(CONFIG.selectors.handles)
};
/**
* GŁÓWNY KONTROLER APLIKACJI
*/
const App = {
init() {
this.renderInfo();
this.bindGlobalEvents();
this.bindZoomHandlers();
},
// Wyświetlenie formatu na toolbarze
renderInfo() {
const { adminW, adminH } = CONFIG.dims;
$('#formatInfo').text(`Format: ${adminW}×${adminH} cm`);
},
// Obliczanie i centrowanie ramki wyboru
centerSelection() {
const { adminW, adminH } = CONFIG.dims;
const ratio = adminW / adminH;
DOM.elSelection.aspectRatio = ratio;
const rect = DOM.elCanvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
// Maksymalnie 70% obszaru
const maxW = rect.width * 0.7;
const maxH = rect.height * 0.7;
let selW, selH;
// Dopasowanie do węższego wymiaru
if (maxW / maxH > ratio) {
selH = maxH;
selW = selH * ratio;
} else {
selW = maxW;
selH = selW / ratio;
}
const x = (rect.width - selW) / 2;
const y = (rect.height - selH) / 2;
DOM.elSelection.$change(x, y, selW, selH, ratio);
},
// Obsługa zdarzeń globalnych (pliki, przyciski)
bindGlobalEvents() {
// 1. Wczytanie pliku
DOM.$input.on('change', function() {
const [file] = this.files;
if (!file) return;
if (!/^image\/\w+$/.test(file.type)) {
alert("To nie jest plik graficzny!");
return;
}
DOM.$name.text(file.name);
DOM.elImage.src = URL.createObjectURL(file);
// Pokaż UI
DOM.$wrapper.fadeIn();
DOM.$uploadBtn.fadeIn();
DOM.$toolbar.addClass('show');
// Inicjalizacja croppera
DOM.elImage.$ready(() => {
DOM.elImage.$center('cover');
App.centerSelection();
});
});
// 2. Toolbar - Przyciski
$('#btnZoomIn').on('click', () => DOM.elImage.$zoom(0.1));
$('#btnZoomOut').on('click', () => DOM.elImage.$zoom(-0.1));
$('#btnReset').on('click', () => DOM.elImage.$center('cover'));
// 3. Zoom kółkiem myszy
DOM.$wrapper.on('wheel', (e) => {
e.preventDefault();
const delta = e.originalEvent.deltaY;
DOM.elImage.$zoom(delta < 0 ? 0.08 : -0.08);
});
// 4. Resize okna przeglądarki
window.addEventListener('resize', () => {
if (DOM.$wrapper.is(':visible')) App.centerSelection();
});
// 5. Upload (Zapisz)
DOM.$uploadBtn.on('click', this.handleUpload);
},
// Logika "Ciągnij za rogi by zmienić zoom"
bindZoomHandlers() {
const onPointerDown = (e) => {
State.isResizing = true;
State.startY = e.clientY;
// Sprawdzamy czy to dolny uchwyt (bl, br) używając dataset lub klasy
State.isInverted = e.target.dataset.pos === 'bottom';
e.preventDefault();
e.stopPropagation();
document.addEventListener('pointermove', onPointerMove, { passive: false });
document.addEventListener('pointerup', onPointerUp, { passive: false, once: true });
};
const onPointerMove = (e) => {
if (!State.isResizing) return;
let dy = e.clientY - State.startY;
State.startY = e.clientY;
// Jeśli dolny uchwyt -> odwracamy znak
if (State.isInverted) dy = -dy;
// Oblicz zoom
const step = (-dy) * CONFIG.zoomSensitivity;
if (step !== 0) DOM.elImage.$zoom(step);
e.preventDefault();
};
const onPointerUp = () => {
State.isResizing = false;
document.removeEventListener('pointermove', onPointerMove);
};
// Podpięcie pod wszystkie uchwyty
DOM.elsHandles.forEach(el => {
el.addEventListener('pointerdown', onPointerDown, { passive: false });
});
},
// Logika wysyłki na serwer
handleUpload() {
const btn = $(this);
btn.prop('disabled', true).text('Przetwarzanie...');
const { outputW, outputH, adminW, adminH } = CONFIG.dims;
// Generowanie canvasu
DOM.elSelection.$toCanvas({ width: outputW, height: outputH })
.then(canvas => {
canvas.toBlob(blob => {
const formData = new FormData();
// Destrukturyzacja do dodania danych
formData.append("original_image", DOM.$input[0].files[0]);
formData.append("cropped_image", blob, "cropped.jpg");
// Metadane
const meta = { product_id: 123, admin_w_cm: adminW, admin_h_cm: adminH, out_w_px: outputW, out_h_px: outputH };
Object.entries(meta).forEach(([key, val]) => formData.append(key, val));
// AJAX
$.ajax({
url: "product-img-save.php",
method: "POST",
data: formData,
processData: false,
contentType: false,
success: (resp) => {
DOM.$result.html(resp);
App.resetUI();
},
error: () => {
alert("Błąd zapisu!");
btn.prop('disabled', false).text('Przytnij i Zapisz');
}
});
}, "image/jpeg", 0.92);
})
.catch(() => {
alert("Błąd generowania obrazu.");
btn.prop('disabled', false).text('Przytnij i Zapisz');
});
},
// Reset po udanym wgraniu
resetUI() {
DOM.$wrapper.hide();
DOM.$toolbar.removeClass('show');
DOM.$uploadBtn.hide().prop('disabled', false).text('Przytnij i Zapisz');
DOM.$input.val('');
DOM.$name.text('');
}
};
// START
$(document).ready(() => App.init());
</script>
</body>
</html>