Update structure

This commit is contained in:
2026-01-15 13:51:21 +01:00
parent 366329d326
commit 149627ef69
2 changed files with 304 additions and 227 deletions

View File

@@ -1,12 +1,12 @@
<?php <?php
// upload.php // upload.php
// Налаштування папок (Ustawienia katalogów) // Ustawienia katalogów
$uploadDir = 'product_uploads/' . $_POST['product_id'] . '/'; $uploadDir = 'product_uploads/' . $_POST['product_id'] . '/';
$originalDir = $uploadDir . 'originals/'; $originalDir = $uploadDir . 'originals/';
$croppedDir = $uploadDir . 'cropped/'; $croppedDir = $uploadDir . 'cropped/';
// Створюємо папки, якщо немає (Tworzenie katalogów, jeśli nie istnieją) // Tworzenie katalogów, jeśli nie istnieją
if (!file_exists($originalDir)) mkdir($originalDir, 0777, true); if (!file_exists($originalDir)) mkdir($originalDir, 0777, true);
if (!file_exists($croppedDir)) mkdir($croppedDir, 0777, true); if (!file_exists($croppedDir)) mkdir($croppedDir, 0777, true);
@@ -32,7 +32,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
// Відповідь клієнту (Odpowiedź) // Odpowiedź
if ($successOriginal && $successCropped) { if ($successOriginal && $successCropped) {
echo "<div style='color:green; border: 1px solid green; padding: 10px;'>"; echo "<div style='color:green; border: 1px solid green; padding: 10px;'>";
echo "<strong>Zapisano pomyślnie!</strong><br>"; echo "<strong>Zapisano pomyślnie!</strong><br>";
@@ -40,9 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
echo "Wykadrowane: " . $croppedPath; echo "Wykadrowane: " . $croppedPath;
echo "</div>"; echo "</div>";
// Тут можна додати SQL запит до БД (Tutaj można dodać zapytanie SQL do bazy danych) // Tutaj można dodać zapytanie SQL do bazy danych)
// Np.: INSERT INTO product_images (original, cropped) VALUES ('$originalPath', '$croppedPath');
} else { } else {
echo "<div style='color:red;'>Wystąpił błąd podczas zapisywania plików.</div>"; echo "<div style='color:red;'>Wystąpił błąd podczas zapisywania plików.</div>";
} }

View File

@@ -3,180 +3,239 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Przesyłanie i kadrowanie zdjęć</title> <title>Edytor Zdjęć Produktowych</title>
<style> <style>
body { font-family: sans-serif; padding: 20px; } /* --- GŁÓWNE STYLE --- */
body { font-family: sans-serif; padding: 20px; color: #333; }
h2 { margin-bottom: 20px; }
/* --- WRAPPER CROPPERA --- */
.cropper-wrapper { .cropper-wrapper {
position: relative;
width: 100%; width: 100%;
height: 500px; height: 500px;
background: #333; background: #222;
margin-bottom: 10px; margin-bottom: 15px;
display: none; border-radius: 8px;
position: relative;
overflow: hidden; overflow: hidden;
border-radius: 6px; display: none; /* Ukryty domyślnie */
user-select: none; user-select: none;
touch-action: none; /* важливо для drag на мобільних */ touch-action: none;
} }
cropper-canvas { width: 100%; height: 100%; } cropper-canvas { width: 100%; height: 100%; }
.btn { /* Stała ramka wyboru */
padding: 10px 16px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
border-radius: 6px;
}
.btn:disabled { background-color: #ccc; cursor: not-allowed; }
/* Червона рамка і не блокує drag */
cropper-selection { cropper-selection {
outline: 3px solid red !important; outline: 2px solid #e74c3c !important; /* Czerwona ramka */
pointer-events: none; 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 { #toolbar {
display: none; display: none;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
margin: 10px 0 15px 0;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 20px;
} }
#toolbar.show { display: flex; } #toolbar.show { display: flex; }
#toolbar .btn { padding: 8px 14px; font-size: 16px; }
#hint { color: #444; font-size: 14px; margin-left: 6px; } .btn {
.chip { padding: 8px 16px;
padding: 6px 10px; background-color: #007bff;
border: 1px solid #ddd; color: white;
border-radius: 999px; border: none;
font-size: 13px; cursor: pointer;
background: #f7f7f7; font-size: 15px;
}
/* ====== РУЧКИ "RESIZE FOTO" (overlay) ====== */
.photo-handles {
position: absolute;
inset: 0;
pointer-events: none; /* включимо лише коли активні */
z-index: 20;
}
.photo-handles.active {
pointer-events: auto;
}
.handle {
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
border: 2px solid #007bff;
box-shadow: 0 1px 4px rgba(0,0,0,0.35);
pointer-events: auto;
}
.handle.nw { left: 10px; top: 10px; cursor: nwse-resize; }
.handle.ne { right: 10px; top: 10px; cursor: nesw-resize; }
.handle.sw { left: 10px; bottom: 10px; cursor: nesw-resize; }
.handle.se { right: 10px; bottom: 10px; cursor: nwse-resize; }
/* маленька рамка для “фото-режиму” */
.photo-outline {
position: absolute;
inset: 6px;
border: 2px dashed rgba(255,255,255,0.5);
border-radius: 6px; border-radius: 6px;
pointer-events: none; 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> </style>
</head> </head>
<body> <body>
<h2>Przesyłanie zdjęcia produktu</h2> <h2>Przesyłanie zdjęcia produktu</h2>
<div style="margin-bottom: 20px;"> <div style="margin-bottom: 20px;">
<label for="inputImage">Wybierz zdjęcie:</label> <label class="btn" for="inputImage">📂 Wybierz zdjęcie</label>
<input type="file" id="inputImage" accept="image/*" /> <input type="file" id="inputImage" accept="image/*" style="display:none" />
<span id="fileName" style="margin-left: 10px; color:#555;"></span>
</div> </div>
<div class="cropper-wrapper" id="cropperWrapper"> <div class="cropper-wrapper" id="cropperWrapper">
<cropper-canvas id="my-cropper" background> <cropper-canvas id="my-cropper" background>
<cropper-image <cropper-image
id="cropperImage" id="cropperImage"
initial-center-size="cover" initial-center-size="cover"
translatable translatable
scalable scalable
></cropper-image> ></cropper-image>
<!-- Drag obrazu -->
<cropper-handle action="move" plain></cropper-handle> <cropper-handle action="move" plain></cropper-handle>
<!-- Stała ramka (format admina ustawiamy w JS) -->
<cropper-selection id="cropperSelection" initial-coverage="0.6"></cropper-selection> <cropper-selection id="cropperSelection" initial-coverage="0.6"></cropper-selection>
</cropper-canvas> </cropper-canvas>
<!-- Overlay: “resize foto za rogi” --> <div class="photo-handles" id="photoHandles">
<div class="photo-handles active" id="photoHandles">
<div class="photo-outline"></div> <div class="photo-outline"></div>
<div class="handle nw" data-corner="nw" title="Zmień rozmiar zdjęcia"></div> <div class="handle tl" data-pos="top" title="Zmień rozmiar"></div>
<div class="handle ne" data-corner="ne" title="Zmień rozmiar zdjęcia"></div> <div class="handle tr" data-pos="top" title="Zmień rozmiar"></div>
<div class="handle sw" data-corner="sw" title="Zmień rozmiar zdjęcia"></div> <div class="handle bl" data-pos="bottom" title="Zmień rozmiar"></div>
<div class="handle se" data-corner="se" title="Zmień rozmiar zdjęcia"></div> <div class="handle br" data-pos="bottom" title="Zmień rozmiar"></div>
</div> </div>
</div> </div>
<div id="toolbar"> <div id="toolbar">
<button type="button" class="btn" id="zoomIn">+</button> <button type="button" class="btn" id="btnZoomIn">+</button>
<button type="button" class="btn" id="zoomOut">-</button> <button type="button" class="btn" id="btnZoomOut">-</button>
<button type="button" class="btn" id="resetImg">Reset</button> <button type="button" class="btn" id="btnReset">Reset</button>
<span class="chip" id="formatInfo"></span> <span class="chip" id="formatInfo"></span>
<span id="hint">Przesuwaj zdjęcie myszą/palcem, zoom kółkiem lub złap za rogi</span> <span id="hint">Mysz/Dotyk: Przesuń | Kółko/Rogi: Zoom</span>
</div> </div>
<button id="btnUpload" class="btn" style="display:none;">Przytnij i Zapisz</button> <button id="btnUpload" class="btn" style="display:none; width: 100%; padding: 12px;">Przytnij i Zapisz</button>
<div id="result" style="margin-top: 20px;"></div> <div id="result" style="margin-top: 20px;"></div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <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 src="https://unpkg.com/cropperjs@2.1.0/dist/cropper.js"></script>
<script> <script>
$(document).ready(function () { /**
const $inputImage = $("#inputImage"); * KONFIGURACJA
const $cropperWrapper = $("#cropperWrapper"); * Wszystkie stałe w jednym miejscu dla łatwej edycji.
const $btnUpload = $("#btnUpload"); */
const $toolbar = $("#toolbar"); 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'
}
};
const cropperCanvas = document.getElementById("my-cropper"); /**
const cropperSelection = document.getElementById("cropperSelection"); * STATE
const cropperImage = document.getElementById("cropperImage"); * Przechowywanie stanu aplikacji
*/
const State = {
isResizing: false,
startY: 0,
isInverted: false // Czy używamy dolnych uchwytów (odwrócona logika)
};
const photoHandles = document.getElementById("photoHandles"); /**
* 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)
};
// ADMIN (z serwera) /**
const ADMIN_W_CM = 8; * GŁÓWNY KONTROLER APLIKACJI
const ADMIN_H_CM = 12; */
const App = {
init() {
this.renderInfo();
this.bindGlobalEvents();
this.bindZoomHandlers();
},
// wynik px // Wyświetlenie formatu na toolbarze
const OUT_W_PX = 800; renderInfo() {
const OUT_H_PX = 1200; const { adminW, adminH } = CONFIG.dims;
$('#formatInfo').text(`Format: ${adminW}×${adminH} cm`);
},
$("#formatInfo").text(`Format: ${ADMIN_W_CM}×${ADMIN_H_CM} cm`); // Obliczanie i centrowanie ramki wyboru
centerSelection() {
const { adminW, adminH } = CONFIG.dims;
const ratio = adminW / adminH;
DOM.elSelection.aspectRatio = ratio;
function centerFixedSelection() { const rect = DOM.elCanvas.getBoundingClientRect();
const ratio = ADMIN_W_CM / ADMIN_H_CM;
cropperSelection.aspectRatio = ratio;
const rect = cropperCanvas.getBoundingClientRect();
if (!rect.width || !rect.height) return; if (!rect.width || !rect.height) return;
// Maksymalnie 70% obszaru
const maxW = rect.width * 0.7; const maxW = rect.width * 0.7;
const maxH = rect.height * 0.7; const maxH = rect.height * 0.7;
let selW, selH; let selW, selH;
// Dopasowanie do węższego wymiaru
if (maxW / maxH > ratio) { if (maxW / maxH > ratio) {
selH = maxH; selH = maxH;
selW = selH * ratio; selW = selH * ratio;
@@ -188,136 +247,156 @@
const x = (rect.width - selW) / 2; const x = (rect.width - selW) / 2;
const y = (rect.height - selH) / 2; const y = (rect.height - selH) / 2;
cropperSelection.$change(x, y, selW, selH, ratio); DOM.elSelection.$change(x, y, selW, selH, ratio);
} },
function bindToolbar() { // Obsługa zdarzeń globalnych (pliki, przyciski)
$("#zoomIn").off("click").on("click", () => cropperImage.$zoom(0.1)); bindGlobalEvents() {
$("#zoomOut").off("click").on("click", () => cropperImage.$zoom(-0.1)); // 1. Wczytanie pliku
$("#resetImg").off("click").on("click", () => cropperImage.$center("cover")); DOM.$input.on('change', function() {
} const [file] = this.files;
if (!file) return;
// Wheel zoom if (!/^image\/\w+$/.test(file.type)) {
$cropperWrapper.on("wheel", function (e) { alert("To nie jest plik graficzny!");
e.preventDefault(); return;
const delta = e.originalEvent.deltaY; }
cropperImage.$zoom(delta < 0 ? 0.08 : -0.08);
});
// ===== "Resize foto za rogi" через zoom ===== DOM.$name.text(file.name);
let isResizingPhoto = false; DOM.elImage.src = URL.createObjectURL(file);
let startY = 0;
function onPointerDown(e) { // Pokaż UI
isResizingPhoto = true; DOM.$wrapper.fadeIn();
startY = e.clientY; DOM.$uploadBtn.fadeIn();
e.preventDefault(); DOM.$toolbar.addClass('show');
e.stopPropagation();
document.addEventListener("pointermove", onPointerMove, { passive: false });
document.addEventListener("pointerup", onPointerUp, { passive: false, once: true });
}
function onPointerMove(e) { // Inicjalizacja croppera
if (!isResizingPhoto) return; DOM.elImage.$ready(() => {
DOM.elImage.$center('cover');
const dy = e.clientY - startY; App.centerSelection();
startY = e.clientY; });
// dy < 0 => zoom in, dy > 0 => zoom out
const step = (-dy) * 0.004; // czułość
if (step !== 0) cropperImage.$zoom(step);
e.preventDefault();
}
function onPointerUp() {
isResizingPhoto = false;
document.removeEventListener("pointermove", onPointerMove);
}
// podepnij do 4 uchwytów
photoHandles.querySelectorAll(".handle").forEach((el) => {
el.addEventListener("pointerdown", onPointerDown, { passive: false });
});
// Upload
$inputImage.change(function () {
const files = this.files;
if (!files || !files.length) return;
const file = files[0];
if (!/^image\/\w+$/.test(file.type)) {
alert("Proszę wybrać plik obrazu.");
return;
}
const uploadedImageURL = URL.createObjectURL(file);
cropperImage.src = uploadedImageURL;
$cropperWrapper.show();
$btnUpload.show();
$toolbar.addClass("show");
bindToolbar();
cropperImage.$ready(() => {
cropperImage.$center("cover");
centerFixedSelection();
}); });
});
window.addEventListener("resize", () => { // 2. Toolbar - Przyciski
if ($cropperWrapper.is(":visible")) centerFixedSelection(); $('#btnZoomIn').on('click', () => DOM.elImage.$zoom(0.1));
}); $('#btnZoomOut').on('click', () => DOM.elImage.$zoom(-0.1));
$('#btnReset').on('click', () => DOM.elImage.$center('cover'));
// Save // 3. Zoom kółkiem myszy
$btnUpload.click(function () { DOM.$wrapper.on('wheel', (e) => {
$btnUpload.prop("disabled", true).text("Przetwarzanie..."); e.preventDefault();
const delta = e.originalEvent.deltaY;
DOM.elImage.$zoom(delta < 0 ? 0.08 : -0.08);
});
cropperSelection // 4. Resize okna przeglądarki
.$toCanvas({ width: OUT_W_PX, height: OUT_H_PX }) window.addEventListener('resize', () => {
.then((canvas) => { if (DOM.$wrapper.is(':visible')) App.centerSelection();
canvas.toBlob( });
(blob) => {
const formData = new FormData();
formData.append("original_image", $inputImage[0].files[0]);
formData.append("cropped_image", blob, "cropped.jpg");
formData.append("product_id", "123");
formData.append("admin_w_cm", ADMIN_W_CM); // 5. Upload (Zapisz)
formData.append("admin_h_cm", ADMIN_H_CM); DOM.$uploadBtn.on('click', this.handleUpload);
formData.append("out_w_px", OUT_W_PX); },
formData.append("out_h_px", OUT_H_PX);
$.ajax({ // Logika "Ciągnij za rogi by zmienić zoom"
url: "product-img-save.php", bindZoomHandlers() {
method: "POST", const onPointerDown = (e) => {
data: formData, State.isResizing = true;
processData: false, State.startY = e.clientY;
contentType: false, // Sprawdzamy czy to dolny uchwyt (bl, br) używając dataset lub klasy
success: function (response) { State.isInverted = e.target.dataset.pos === 'bottom';
$("#result").html(response);
$cropperWrapper.hide(); e.preventDefault();
$toolbar.removeClass("show"); e.stopPropagation();
$btnUpload.hide().prop("disabled", false).text("Przytnij i Zapisz");
$inputImage.val(""); document.addEventListener('pointermove', onPointerMove, { passive: false });
}, document.addEventListener('pointerup', onPointerUp, { passive: false, once: true });
error: function () { };
alert("Błąd podczas przesyłania.");
$btnUpload.prop("disabled", false).text("Przytnij i Zapisz"); const onPointerMove = (e) => {
}, if (!State.isResizing) return;
});
}, let dy = e.clientY - State.startY;
"image/jpeg", State.startY = e.clientY;
0.92
); // 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(() => { .catch(() => {
alert("Nie udało się przygotować obrazu do zapisu."); alert("Błąd generowania obrazu.");
$btnUpload.prop("disabled", false).text("Przytnij i Zapisz"); 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> </script>
</body> </body>
</html> </html>