diff --git a/api-comments.php b/api-comments.php new file mode 100644 index 0000000..6eda297 --- /dev/null +++ b/api-comments.php @@ -0,0 +1,55 @@ + 'error', 'message' => 'Brak ścieżki pliku']); + exit; + } + + try { + $stmt = $pdo->prepare("SELECT * FROM prototype_comments WHERE page_path = ? ORDER BY created_at DESC"); + $stmt->execute([$pagePath]); + $comments = $stmt->fetchAll(); + echo json_encode(['status' => 'success', 'data' => $comments]); + } catch (PDOException $e) { + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); + } + exit; +} + +// 2. DODAWANIE KOMENTARZA (POST) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'add') { + // Odczyt danych JSON z body requestu + $input = json_decode(file_get_contents('php://input'), true); + + $pagePath = $input['page_path'] ?? ''; + $selector = $input['selector'] ?? ''; + $comment = $input['comment'] ?? ''; + $author = $input['author'] ?? 'Anonim'; // Możesz tu potem wpiąć sesję użytkownika + + if (!$pagePath || !$selector || !$comment) { + echo json_encode(['status' => 'error', 'message' => 'Brakuje danych']); + exit; + } + + try { + $stmt = $pdo->prepare("INSERT INTO prototype_comments (page_path, dom_selector, author, comment) VALUES (?, ?, ?, ?)"); + $stmt->execute([$pagePath, $selector, $author, $comment]); + + echo json_encode(['status' => 'success', 'id' => $pdo->lastInsertId()]); + } catch (PDOException $e) { + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); + } + exit; +} +?> \ No newline at end of file diff --git a/assets/css/comments.css b/assets/css/comments.css new file mode 100644 index 0000000..f6eeef3 --- /dev/null +++ b/assets/css/comments.css @@ -0,0 +1,225 @@ +/* assets/css/comments.css */ + +/* Pasek narzędzi na górze */ +#prototype-topbar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 50px; + background: #232f3e; + color: white; + z-index: 100000; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-family: system-ui, -apple-system, sans-serif; +} + +#prototype-topbar .mode-switch { + display: flex; + align-items: center; + gap: 10px; +} + +/* Przełącznik (Toggle) */ +.switch-label { + position: relative; + display: inline-block; + width: 40px; + height: 20px; +} + +.switch-label input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 20px; +} + +.slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked+.slider { + background-color: #2196F3; +} + +input:checked+.slider:before { + transform: translateX(20px); +} + +/* Pinezki (Markery) */ +.comment-marker { + position: absolute; + width: 24px; + height: 24px; + background: #ff3e1d; + border: 2px solid white; + border-radius: 50% 50% 50% 0; + transform: rotate(-45deg) translate(-50%, -100%); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + cursor: pointer; + z-index: 10000; + transition: transform 0.2s; +} + +.comment-marker:hover { + z-index: 10001; + transform: rotate(-45deg) scale(1.2); +} + +.comment-marker.resolved { + background: #71dd37; + /* Zielony dla rozwiązanych */ +} + +/* Dymek z komentarzem (Tooltip) */ +.comment-popover { + position: absolute; + background: white; + border: 1px solid #d9dee3; + border-radius: 6px; + padding: 10px; + width: 250px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + z-index: 10002; + font-size: 13px; + color: #333; +} + +.comment-popover h6 { + margin: 0 0 5px 0; + font-size: 12px; + color: #888; + font-weight: bold; +} + +.comment-popover p { + margin: 0; + line-height: 1.4; +} + +/* Tryb wyboru - podświetlanie elementów */ +.comment-mode-active *:hover { + outline: 2px dashed #2196F3 !important; + cursor: comment !important; +} + +/* Wykluczenia, żeby nie podświetlać samego UI komentarzy */ +.comment-mode-active #prototype-topbar *:hover, +.comment-mode-active .comment-marker:hover, +.comment-mode-active .comment-popover *:hover { + outline: none !important; + cursor: default !important; +} + +/* Modal do dodawania (prosty) */ +#comment-input-box { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + z-index: 100002; + width: 300px; + display: none; +} + +#comment-input-box textarea { + width: 100%; + height: 80px; + margin-bottom: 10px; + padding: 5px; +} + +#comment-input-box-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 100001; + display: none; + backdrop-filter: blur(2px); +} + +/* --- POPRAWKA: Wykluczenie okienka edycji z efektów wizualnych --- */ + +/* Nawet w trybie comment-mode-active, nasze okno dialogowe ma wyglądać normalnie */ +.comment-mode-active #comment-input-box, +.comment-mode-active #comment-input-box * { + outline: none !important; + cursor: auto !important; + /* Przywraca normalny kursor (strzałkę/łapkę) */ +} + +/* --- POPRAWKA: Wykluczenie okienka edycji z efektów wizualnych --- */ + +/* assets/css/comments.css */ + +/* ... poprzednie style bez zmian ... */ + +/* POPRAWKI DLA MODALA I FOCUSU */ +#comment-input-box { + /* Ustawiamy fixed/absolute dynamicznie w JS, ale bazowo: */ + z-index: 100000; + /* Musi być wyżej niż Bootstrap Modal (zwykle 1055) */ + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + width: 320px; + display: none; + /* Centrowanie - teraz zrobimy to sprytniej w CSS */ + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +#comment-input-box-overlay { + position: fixed; + /* JS zmieni na absolute jeśli w modalu */ + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + z-index: 99999; + display: none; +} + +/* Ważne: Pineski muszą być nad wszystkim */ +.comment-marker { + z-index: 100001; + pointer-events: auto !important; + /* Żeby dało się w nie klikać nawet w trybie blokady */ +} \ No newline at end of file diff --git a/assets/js/comments.js b/assets/js/comments.js new file mode 100644 index 0000000..aecba26 --- /dev/null +++ b/assets/js/comments.js @@ -0,0 +1,358 @@ +// assets/js/comments.js + +(function () { + // Konfiguracja API + const baseUrl = (typeof MAGICO_BASE_URL !== 'undefined') ? MAGICO_BASE_URL : ''; + const API_URL = baseUrl + '/api-comments.php'; + const CURRENT_PATH = window.location.pathname.replace(/^\/|\/$/g, ''); + + let isCommentMode = false; + let pendingElement = null; // Element, który chcemy skomentować + let markersData = []; // Przechowujemy dane o markerach lokalnie do szybkiego odświeżania + + // --- 1. GENEROWANIE UI --- + function initUI() { + document.body.style.paddingTop = '50px'; + + const bar = document.createElement('div'); + bar.id = 'prototype-topbar'; + bar.innerHTML = ` +
+ Magico Feedback +
+
+ Tryb Komentowania + +
+ `; + document.body.appendChild(bar); + + // Kontener na input (domyślnie w body) + const modalHtml = ` +
+
+
Dodaj notatkę
+ +
+ + +
+
+ `; + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Listenery UI + document.getElementById('comment-toggle').addEventListener('change', function (e) { + isCommentMode = e.target.checked; + toggleCommentMode(isCommentMode); + }); + + document.getElementById('cancel-comment').addEventListener('click', closeInputBox); + document.getElementById('save-comment').addEventListener('click', saveComment); + + // Scroll listener (capture) - żeby aktualizować pineski przy każdym przewinięciu (okna lub modala) + window.addEventListener('scroll', updateMarkerPositions, true); + } + + // --- 2. LOGIKA TRYBU (AGRESYWNE BLOKOWANIE) --- + function toggleCommentMode(active) { + if (active) { + document.body.classList.add('comment-mode-active'); + // Blokujemy wszystko: click, mousedown, mouseup, submit + ['click', 'mousedown', 'mouseup', 'submit'].forEach(evt => + window.addEventListener(evt, handleInteraction, true) + ); + } else { + document.body.classList.remove('comment-mode-active'); + ['click', 'mousedown', 'mouseup', 'submit'].forEach(evt => + window.removeEventListener(evt, handleInteraction, true) + ); + } + } + + // --- 3. OBSŁUGA INTERAKCJI --- + function handleInteraction(e) { + // ZAWSZE pozwalamy na interakcję z naszym UI + if (e.target.closest('#prototype-topbar') || + e.target.closest('#comment-input-box') || + e.target.closest('.comment-marker')) { + return; + } + + // Blokujemy natywną akcję + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + // Reagujemy tylko na 'click' (żeby nie odpalać input boxa 3 razy na mousedown/up/click) + if (e.type === 'click') { + pendingElement = e.target; + openInputBox(); + } + } + + // --- 4. SELEKTOR CSS --- + function getCssSelector(el) { + if (!(el instanceof Element)) return; + const path = []; + while (el.nodeType === Node.ELEMENT_NODE && el.tagName !== 'BODY') { + let selector = el.tagName.toLowerCase(); + if (el.id) { + selector += '#' + el.id; + path.unshift(selector); + break; + } else { + let sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.tagName.toLowerCase() == selector) nth++; + } + if (nth > 1) selector += ":nth-of-type(" + nth + ")"; + + if (el.classList.length > 0) { + for (let cls of el.classList) { + if (cls !== 'active' && cls !== 'show' && cls !== 'collapsed' && cls !== 'fade') { + selector += "." + cls; + break; + } + } + } + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(" > "); + } + + // --- 5. OBSŁUGA INPUTA (PRZENOSZENIE DO MODALA) --- + function openInputBox() { + const box = document.getElementById('comment-input-box'); + const overlay = document.getElementById('comment-input-box-overlay'); + + // FIX: Sprawdź czy jest otwarty modal Bootstrapa + const activeModal = document.querySelector('.modal.show .modal-content'); + + if (activeModal) { + // Jeśli tak, przenieś nasz box do modala (żeby działało pisanie i focus) + activeModal.appendChild(box); + // Overlay też, żeby przykrył modal + activeModal.appendChild(overlay); + box.style.position = 'absolute'; // W modalu pozycjonujemy absolutnie względem modala + } else { + // Jeśli nie, wracamy do body + document.body.appendChild(overlay); + document.body.appendChild(box); + box.style.position = 'fixed'; + } + + // Tymczasowe wyłączenie trybu wybierania, żeby można było klikać w Boxie + // Ale NIE zdejmujemy listenerów blokujących tło (bo tło nadal ma być nieklikalne) + // Po prostu nasza funkcja handleInteraction przepuszcza kliknięcia w #comment-input-box + + overlay.style.display = 'block'; + box.style.display = 'block'; + + // Focus po małym timeout, żeby przeglądarka zdążyła przenieść element + setTimeout(() => document.getElementById('new-comment-text').focus(), 50); + } + + function closeInputBox() { + const box = document.getElementById('comment-input-box'); + const overlay = document.getElementById('comment-input-box-overlay'); + + overlay.style.display = 'none'; + box.style.display = 'none'; + document.getElementById('new-comment-text').value = ''; + pendingElement = null; + + // Wracamy z boxem do body na wszelki wypadek + document.body.appendChild(overlay); + document.body.appendChild(box); + } + + function saveComment() { + const text = document.getElementById('new-comment-text').value; + if (!text || !pendingElement) { + closeInputBox(); + return; + } + + const selector = getCssSelector(pendingElement); + + fetch(API_URL + '?action=add', { + method: 'POST', + body: JSON.stringify({ + page_path: CURRENT_PATH, + selector: selector, + comment: text, + author: 'User' + }) + }) + .then(res => res.json()) + .then(data => { + if (data.status === 'success') { + closeInputBox(); + loadComments(); + } else { + alert('Błąd: ' + data.message); + } + }); + } + + // --- 6. PINESKI I AKTUALIZACJA POZYCJI --- + function loadComments() { + fetch(API_URL + '?action=list&page_path=' + encodeURIComponent(CURRENT_PATH)) + .then(res => res.json()) + .then(resp => { + if (resp.status === 'success') { + markersData = resp.data; // Zapisz dane + renderMarkers(); // Narysuj + } + }); + } + + let activePopover = null; + + function renderMarkers() { + // Usuń stare + document.querySelectorAll('.comment-marker').forEach(el => el.remove()); + if (activePopover) { activePopover.remove(); activePopover = null; } + + markersData.forEach((c, index) => { + try { + const el = document.querySelector(c.dom_selector); + if (el && el.offsetParent !== null) { // Tylko widoczne elementy + const marker = document.createElement('div'); + marker.className = 'comment-marker'; + marker.dataset.index = index; // Żeby łatwo znaleźć dane + + // Zdarzenie kliknięcia w marker + marker.addEventListener('click', (e) => { + e.preventDefault(); e.stopPropagation(); + showPopover(marker, c); + }); + + document.body.appendChild(marker); + } + } catch (err) { } + }); + + // Po stworzeniu od razu ustaw pozycje + updateMarkerPositions(); + } + + function showPopover(marker, data) { + if (activePopover) activePopover.remove(); + + const pop = document.createElement('div'); + pop.className = 'comment-popover'; + + // Escape HTML - XSS Protection + const safeAuthor = document.createElement('div'); + safeAuthor.textContent = data.author || 'Anonim'; + const safeComment = document.createElement('div'); + safeComment.textContent = data.comment || ''; + + pop.innerHTML = ` +
${safeAuthor.innerHTML}
+

${safeComment.innerHTML}

+ `; + + document.body.appendChild(pop); + activePopover = pop; + pop._associatedMarker = marker; + + // Pozycjonowanie + const rect = marker.getBoundingClientRect(); + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + + pop.style.top = (rect.top + scrollTop - 10) + 'px'; // Trochę wyżej + pop.style.left = (rect.right + scrollLeft + 10) + 'px'; // Obok markera + + // Jeśli wychodzi poza ekran z prawej, daj na lewo + if (rect.right + 260 > window.innerWidth) { + pop.style.left = (rect.left + scrollLeft - 260) + 'px'; + } + } + + // Zamknij popover przy kliknięciu w tło + window.addEventListener('click', (e) => { + if (activePopover && !e.target.closest('.comment-popover') && !e.target.closest('.comment-marker')) { + activePopover.remove(); + activePopover = null; + } + }); + + // Aktualizuj też pozycję otwartego popovera przy scrollu + const originalUpdatePositions = updateMarkerPositions; + // ... w sumie updateMarkerPositions jest niżej zdefiniowana, + // lepiej wrzucimy to w updateMarkerPositions bezpośrednio. + + // Funkcja wywoływana przy scrollowaniu - musi być szybka + function updateMarkerPositions() { + const markers = document.querySelectorAll('.comment-marker'); + + markers.forEach(marker => { + const index = marker.dataset.index; + const data = markersData[index]; + if (!data) return; + + try { + const el = document.querySelector(data.dom_selector); + if (el && el.offsetParent !== null) { + const rect = el.getBoundingClientRect(); + + // Jeśli element wyjechał poza ekran (jest w scrollowanym divie ale schowany) + // Można go ukryć, ale na razie po prostu przesuwamy + marker.style.top = (rect.top + window.scrollY) + 'px'; + marker.style.left = (rect.left + window.scrollX + (rect.width / 2)) + 'px'; + marker.style.display = 'block'; + + if (activePopover && activePopover._associatedMarker === marker) { + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + + // Recalculate position + let top = (rect.top + scrollTop - 10); + let left = (rect.right + scrollLeft + 10); + + if (rect.right + 260 > window.innerWidth) { + left = (rect.left + scrollLeft - 260); // Flip to left + } + + activePopover.style.top = top + 'px'; + activePopover.style.left = left + 'px'; + } + } else { + marker.style.display = 'none'; // Ukryj jeśli element zniknął + } + } catch (e) { } + }); + + // Jeśli jest aktywny popover, zamknij go przy szybkim scrollu żeby nie pływał dziwnie? + // Albo po prostu zostaw. + } + + // START + initUI(); + loadComments(); + + // Odświeżanie przy zmianie rozmiaru okna + window.addEventListener('resize', updateMarkerPositions); + + // MutationObserver - odświeżaj pozycje jak coś się zmieni w DOM (np otwarcie taba) + const observer = new MutationObserver(() => { + // Debounce dla wydajności + if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout); + window.commentUpdateTimeout = setTimeout(renderMarkers, 200); + }); + + observer.observe(document.body, { + childList: true, subtree: true, attributes: true, + attributeFilter: ['class', 'style', 'hidden'] + }); + +})(); \ No newline at end of file diff --git a/db_connect.php b/db_connect.php new file mode 100644 index 0000000..da40b69 --- /dev/null +++ b/db_connect.php @@ -0,0 +1,21 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; + +try { + $pdo = new PDO($dsn, $user, $pass, $options); +} catch (\PDOException $e) { + // W środowisku produkcyjnym nie pokazuj błędów użytkownikom, ale tu to prototyp: + throw new \PDOException($e->getMessage(), (int) $e->getCode()); +} +?> \ No newline at end of file diff --git a/footer-sneat.php b/footer-sneat.php index 1a04b7c..c9bd2c2 100644 --- a/footer-sneat.php +++ b/footer-sneat.php @@ -27,4 +27,13 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/header-sneat.php b/header-sneat.php index 608fa60..2d74032 100644 --- a/header-sneat.php +++ b/header-sneat.php @@ -44,6 +44,10 @@ if ($_SERVER['HTTP_HOST'] === 'localhost' || $_SERVER['HTTP_HOST'] === '127.0.0. + + + + diff --git a/prototype/travel/travel-zapytanie-dialog.php b/prototype/travel/travel-zapytanie-dialog.php index d69f0a1..4e75a78 100644 --- a/prototype/travel/travel-zapytanie-dialog.php +++ b/prototype/travel/travel-zapytanie-dialog.php @@ -1,4 +1,7 @@ - +