diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..81182c3
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,3 @@
+
+ Deny from all
+
diff --git a/api-comments.php b/api-comments.php
index 6eda297..e78d2d4 100644
--- a/api-comments.php
+++ b/api-comments.php
@@ -5,8 +5,41 @@ header('Content-Type: application/json');
// Dołączamy połączenie do bazy (upewnij się, że plik db_connect.php istnieje i ma poprawne dane)
require_once 'db_connect.php';
+session_start();
+
$action = $_GET['action'] ?? '';
+// 0. AUTORYZACJA (LOGOWANIE)
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'auth') {
+ $input = json_decode(file_get_contents('php://input'), true);
+ $pass = $input['password'] ?? '';
+
+ // $PROTOTYPE_PASSWORD jest zdefiniowane w db_connect.php
+ if (isset($PROTOTYPE_PASSWORD) && $pass === $PROTOTYPE_PASSWORD) {
+ $_SESSION['prototype_auth'] = true;
+ echo json_encode(['status' => 'success']);
+ } else {
+ echo json_encode(['status' => 'error', 'message' => 'Nieprawidłowe hasło']);
+ }
+ exit;
+}
+
+// 0. SPRAWDZENIE SESJI (Opcjonalne dla list, wymagane dla add)
+function isAuthorized()
+{
+ return !empty($_SESSION['prototype_auth']);
+}
+
+// 0.5 SPRAWDZENIE STANU AUTORYZACJI (GET) - dla frontendowego odtworzenia sesji
+if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'check_auth') {
+ if (isAuthorized()) {
+ echo json_encode(['status' => 'success']);
+ } else {
+ echo json_encode(['status' => 'error', 'message' => 'Brak sesji']);
+ }
+ exit;
+}
+
// 1. POBIERANIE KOMENTARZY (GET)
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') {
$pagePath = $_GET['page_path'] ?? '';
@@ -20,6 +53,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') {
$stmt = $pdo->prepare("SELECT * FROM prototype_comments WHERE page_path = ? ORDER BY created_at DESC");
$stmt->execute([$pagePath]);
$comments = $stmt->fetchAll();
+
+ // XSS PROTECTION: Escaping danych przed wysłaniem
+ foreach ($comments as &$c) {
+ $c['author'] = htmlspecialchars($c['author'] ?? '', ENT_QUOTES, 'UTF-8');
+ $c['comment'] = htmlspecialchars($c['comment'] ?? '', ENT_QUOTES, 'UTF-8');
+ // Selector musi zostać oryginalny, bo JS go używa do querySelector!
+ // $c['dom_selector'] = htmlspecialchars($c['dom_selector'] ?? '', ENT_QUOTES, 'UTF-8');
+ }
+
echo json_encode(['status' => 'success', 'data' => $comments]);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
@@ -29,6 +71,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') {
// 2. DODAWANIE KOMENTARZA (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'add') {
+ // Wymagana autoryzacja
+ if (!isAuthorized()) {
+ echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji. Odśwież stronę i podaj hasło.']);
+ exit;
+ }
+
// Odczyt danych JSON z body requestu
$input = json_decode(file_get_contents('php://input'), true);
@@ -52,4 +100,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'add') {
}
exit;
}
+
+// 3. ROZWIĄZYWANIE KOMENTARZA (POST)
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'resolve') {
+ if (!isAuthorized()) {
+ echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji']);
+ exit;
+ }
+
+ $input = json_decode(file_get_contents('php://input'), true);
+ $id = $input['id'] ?? 0;
+
+ if (!$id) {
+ echo json_encode(['status' => 'error', 'message' => 'Brak ID']);
+ exit;
+ }
+
+ try {
+ // Zmieniamy flagę is_resolved na 1
+ $stmt = $pdo->prepare("UPDATE prototype_comments SET is_resolved = 1 WHERE id = ?");
+ $stmt->execute([$id]);
+ echo json_encode(['status' => 'success']);
+ } catch (PDOException $e) {
+ echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
+ }
+ exit;
+}
+
+// 4. USUWANIE KOMENTARZA (POST)
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete') {
+ if (!isAuthorized()) {
+ echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji']);
+ exit;
+ }
+
+ $input = json_decode(file_get_contents('php://input'), true);
+ $id = $input['id'] ?? 0;
+
+ if (!$id) {
+ echo json_encode(['status' => 'error', 'message' => 'Brak ID']);
+ exit;
+ }
+
+ try {
+ $stmt = $pdo->prepare("DELETE FROM prototype_comments WHERE id = ?");
+ $stmt->execute([$id]);
+ echo json_encode(['status' => 'success']);
+ } 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
index f6eeef3..43abfda 100644
--- a/assets/css/comments.css
+++ b/assets/css/comments.css
@@ -87,7 +87,7 @@ input:checked+.slider:before {
.comment-marker:hover {
z-index: 10001;
- transform: rotate(-45deg) scale(1.2);
+ transform: rotate(-45deg) translate(-50%, -100%) scale(1.2);
}
.comment-marker.resolved {
diff --git a/assets/js/comments.js b/assets/js/comments.js
index aecba26..f1446a5 100644
--- a/assets/js/comments.js
+++ b/assets/js/comments.js
@@ -45,11 +45,43 @@
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Listenery UI
- document.getElementById('comment-toggle').addEventListener('change', function (e) {
- isCommentMode = e.target.checked;
- toggleCommentMode(isCommentMode);
+ // Listenery UI
+ const toggle = document.getElementById('comment-toggle');
+
+ toggle.addEventListener('change', function (e) {
+ if (e.target.checked) {
+ // Próba włączenia - autoryzacja
+ checkAuth().then(ok => {
+ if (ok) {
+ isCommentMode = true;
+ localStorage.setItem('magico_comment_mode', 'true');
+ toggleCommentMode(true);
+ } else {
+ e.target.checked = false; // Cofnij switch
+ }
+ });
+ } else {
+ isCommentMode = false;
+ localStorage.removeItem('magico_comment_mode');
+ toggleCommentMode(false);
+ }
});
+ // --- RESTORE SESSION ---
+ const savedMode = localStorage.getItem('magico_comment_mode');
+ if (savedMode === 'true') {
+ // Sprawdź cicho czy sesja PHP jest aktywna
+ checkAuth(true).then(ok => {
+ if (ok) {
+ toggle.checked = true;
+ isCommentMode = true;
+ toggleCommentMode(true);
+ } else {
+ localStorage.removeItem('magico_comment_mode'); // Sesja wygasła
+ }
+ });
+ }
+
document.getElementById('cancel-comment').addEventListener('click', closeInputBox);
document.getElementById('save-comment').addEventListener('click', saveComment);
@@ -57,6 +89,51 @@
window.addEventListener('scroll', updateMarkerPositions, true);
}
+ // --- 1.5 AUTORYZACJA ---
+ // Prosta weryfikacja - czy mamy flagę w sesji JS?
+ // Lepiej: przy włączeniu zapytać o hasło jeśli nie mamy, i wysłać do API.
+ // API ustawi sesję PHP.
+
+ let isAuthorized = false; // Lokalna flaga, aby nie pytać co chwilę
+
+ async function checkAuth(silent = false) {
+ if (isAuthorized) return true;
+
+ // Najpierw zapytaj API czy już jesteśmy zalogowani (sesja PHP)
+ try {
+ const check = await fetch(API_URL + '?action=check_auth');
+ const checkData = await check.json();
+ if (checkData.status === 'success') {
+ isAuthorized = true;
+ return true;
+ }
+ } catch (e) { }
+
+ if (silent) return false;
+
+ const pass = prompt("Podaj hasło do trybu komentowania:");
+ if (!pass) return false;
+
+ try {
+ const res = await fetch(API_URL + '?action=auth', {
+ method: 'POST',
+ body: JSON.stringify({ password: pass })
+ });
+ const data = await res.json();
+
+ if (data.status === 'success') {
+ isAuthorized = true;
+ return true;
+ } else {
+ alert("Błąd: " + (data.message || "Nieprawidłowe hasło"));
+ return false;
+ }
+ } catch (e) {
+ alert("Błąd połączenia z API");
+ return false;
+ }
+ }
+
// --- 2. LOGIKA TRYBU (AGRESYWNE BLOKOWANIE) ---
function toggleCommentMode(active) {
if (active) {
@@ -220,18 +297,58 @@
document.querySelectorAll('.comment-marker').forEach(el => el.remove());
if (activePopover) { activePopover.remove(); activePopover = null; }
- markersData.forEach((c, index) => {
+ // Grupowanie komentarzy po selektorze
+ const grouped = {};
+ markersData.forEach((c) => {
+ // Pomiń rozwiązane, jeśli chcemy je ukrywać (na razie pokazujemy wszystkie, albo tylko nierozwiązane?)
+ // User: "archiwum zmienia flagę is_resolved". Zakładamy że archiwum = ukryte?
+ // "Archiwum" zwykle oznacza ukryte. Ukryjmy rozwiązane.
+ if (c.is_resolved == 1) return;
+
+ if (!grouped[c.dom_selector]) grouped[c.dom_selector] = [];
+ grouped[c.dom_selector].push(c);
+ });
+
+ Object.keys(grouped).forEach((selector, index) => {
+ const commentsList = grouped[selector];
+ if (!commentsList.length) return;
+
try {
- const el = document.querySelector(c.dom_selector);
+ const el = document.querySelector(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
+ marker.dataset.selector = selector;
- // Zdarzenie kliknięcia w marker
- marker.addEventListener('click', (e) => {
- e.preventDefault(); e.stopPropagation();
- showPopover(marker, c);
+ // Jeśli więcej niż 1 komentarz, pokaż licznik
+ if (commentsList.length > 1) {
+ marker.textContent = commentsList.length;
+ marker.style.display = 'flex';
+ marker.style.alignItems = 'center';
+ marker.style.justifyContent = 'center';
+ marker.style.color = 'white';
+ marker.style.fontSize = '12px';
+ marker.style.fontWeight = 'bold';
+ // Resetuejmu transform dla tekstu żeby był czytelny? Nie, rotacja jest na pinezce.
+ // Tekst też się obróci. Trzeba by go odkręcić.
+ // Prościej: dodajmy span w środku który odkręcimy.
+ marker.innerHTML = `${commentsList.length}`;
+ }
+
+ // Zdarzenie HOVER (najazd myszką)
+ marker.addEventListener('mouseenter', (e) => {
+ showPopover(marker, commentsList);
+ });
+
+ marker.addEventListener('mouseleave', (e) => {
+ marker._leaveTimeout = setTimeout(() => {
+ if (activePopover && activePopover._associatedMarker === marker) {
+ if (!activePopover.matches(':hover')) {
+ activePopover.remove();
+ activePopover = null;
+ }
+ }
+ }, 300);
});
document.body.appendChild(marker);
@@ -243,48 +360,96 @@
updateMarkerPositions();
}
- function showPopover(marker, data) {
+ function showPopover(marker, commentsList) {
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 || '';
+ // Budowanie listy komentarzy
+ let html = '
';
- pop.innerHTML = `
-
${safeAuthor.innerHTML}
-
${safeComment.innerHTML}
- `;
+ commentsList.forEach(data => {
+ // Escape (chociaż API to robi, warto mieć warstwę w JS w razie czego, ale API już robi htmlspecialchars)
+ // Skoro API robi htmlspecialchars, to tutaj możemy bezpiecznie wstawić
+ // Ale uwaga: JS textContent jest bezpieczniejszy.
+ // Hack na szybkie budowanie bezpiecznego HTML w pętli stringów jest trudny.
+ // Zbudujmy to jako elementy DOM potem? Albo zaufajmy API + textContent buildera.
+
+ // Zrobimy placeholder i podstawimy wartości.
+
+ html += `
+
+ `;
+ });
+ html += '
';
+
+ pop.innerHTML = html;
document.body.appendChild(pop);
activePopover = pop;
pop._associatedMarker = marker;
+ // Bindowanie akcji
+ pop.querySelectorAll('.btn-resolve').forEach(btn => {
+ btn.addEventListener('click', () => resolveComment(btn.dataset.id));
+ });
+ pop.querySelectorAll('.btn-delete').forEach(btn => {
+ btn.addEventListener('click', () => deleteComment(btn.dataset.id));
+ });
+
+ // Obsługa zamykania przy wyjechaniu z popovera
+ pop.addEventListener('mouseleave', () => {
+ if (activePopover === pop) {
+ activePopover.remove();
+ activePopover = null;
+ }
+ });
+
// 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
+ pop.style.top = (rect.top + scrollTop - 5) + 'px';
+ pop.style.left = (rect.right + scrollLeft + 2) + 'px';
- // 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;
- }
- });
+ function resolveComment(id) {
+ if (!confirm('Czy na pewno chcesz rozwiązać/zarchiwizować ten komentarz?')) return;
+ fetch(API_URL + '?action=resolve', {
+ method: 'POST', body: JSON.stringify({ id: id })
+ }).then(res => res.json()).then(res => {
+ if (res.status === 'success') loadComments();
+ else alert('Błąd: ' + res.message);
+ });
+ }
+
+ function deleteComment(id) {
+ if (!confirm('Czy na pewno chcesz trwale usunąć ten komentarz?')) return;
+ fetch(API_URL + '?action=delete', {
+ method: 'POST', body: JSON.stringify({ id: id })
+ }).then(res => res.json()).then(res => {
+ if (res.status === 'success') loadComments();
+ else alert('Błąd: ' + res.message);
+ });
+ }
+
+ // (Usunięto click-outside listener, bo teraz działamy na hover)
// Aktualizuj też pozycję otwartego popovera przy scrollu
const originalUpdatePositions = updateMarkerPositions;
@@ -296,44 +461,47 @@
const markers = document.querySelectorAll('.comment-marker');
markers.forEach(marker => {
- const index = marker.dataset.index;
- const data = markersData[index];
- if (!data) return;
+ const selector = marker.dataset.selector;
+ if (!selector) return;
try {
- const el = document.querySelector(data.dom_selector);
+ const el = document.querySelector(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';
+ marker.style.display = (marker.textContent.length > 0) ? 'flex' : 'block';
+ // Flex dla licznika, block dla zwykłego? A w stylach mamy display?
+ // W renderMarkers ustawilismy display:flex ręcznie dla licznika.
+ // Tu musimy uwazac zeby tego nie zepsuc. Pominmy style.display='block' jesli ma flex.
+ if (marker.style.display !== 'flex') 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);
+ let top = (rect.top + scrollTop - 5);
+ let left = (rect.right + scrollLeft + 2);
if (rect.right + 260 > window.innerWidth) {
- left = (rect.left + scrollLeft - 260); // Flip to left
+ left = (rect.left + scrollLeft - 260);
}
activePopover.style.top = top + 'px';
activePopover.style.left = left + 'px';
}
} else {
- marker.style.display = 'none'; // Ukryj jeśli element zniknął
+ marker.style.display = 'none';
+ if (activePopover && activePopover._associatedMarker === marker) {
+ activePopover.remove();
+ activePopover = null;
+ }
}
} catch (e) { }
});
-
- // Jeśli jest aktywny popover, zamknij go przy szybkim scrollu żeby nie pływał dziwnie?
- // Albo po prostu zostaw.
}
// START
@@ -343,11 +511,77 @@
// 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);
+ // MutationObserver - odświeżaj pozycje jak coś się zmieni w wygenerowanym DOM (ale ignoruj nasze markery)
+ // Helper sprawdzający czy element jest częścią naszego UI
+ function isOurElement(node) {
+ if (!node || node.nodeType !== 1) return false;
+
+ if (node.classList && (
+ node.classList.contains('comment-marker') ||
+ node.classList.contains('comment-popover') ||
+ node.id === 'comment-input-box' ||
+ node.id === 'comment-input-box-overlay'
+ )) return true;
+
+ if (node.closest && (
+ node.closest('.comment-marker') ||
+ node.closest('.comment-popover') ||
+ node.closest('#comment-input-box')
+ )) return true;
+
+ return false;
+ }
+
+ const observer = new MutationObserver((mutations) => {
+ let shouldUpdate = false;
+
+ for (const mutation of mutations) {
+ // 1. Zmiany atrybutów na naszych elementach
+ if (mutation.type === 'attributes') {
+ if (isOurElement(mutation.target)) continue;
+ }
+
+ // 2. Zmiany w strukturze DOM (dodawanie/usuwanie węzłów)
+ if (mutation.type === 'childList') {
+ let externalChanges = false;
+
+ // Sprawdź dodane
+ if (mutation.addedNodes.length > 0) {
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
+ const node = mutation.addedNodes[i];
+ if (node.nodeType === 1 && !isOurElement(node)) {
+ externalChanges = true; break;
+ }
+ // Ignorujemy same zmiany tekstowe/puste znaki w body, chyba że to istotne?
+ // Dla bezpieczeństwa: jeśli to tekst i nie jest pusty -> update
+ if (node.nodeType === 3 && node.textContent.trim() !== '') {
+ externalChanges = true; break;
+ }
+ }
+ }
+
+ // Sprawdź usunięte (tylko jeśli jeszcze nie wykryto zmian)
+ if (!externalChanges && mutation.removedNodes.length > 0) {
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
+ const node = mutation.removedNodes[i];
+ if (node.nodeType === 1 && !isOurElement(node)) {
+ externalChanges = true; break;
+ }
+ }
+ }
+
+ // Jeśli zmiany nie dotyczyły "zewnętrznych" elementów (czyli tylko nasze), ignoruj.
+ if (!externalChanges) continue;
+ }
+
+ shouldUpdate = true;
+ break;
+ }
+
+ if (shouldUpdate) {
+ if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout);
+ window.commentUpdateTimeout = setTimeout(renderMarkers, 200);
+ }
});
observer.observe(document.body, {
diff --git a/db_connect.php b/db_connect.php
index da40b69..d93472f 100644
--- a/db_connect.php
+++ b/db_connect.php
@@ -3,8 +3,12 @@ $host = 'localhost';
$db = 'srv105686_magicoprototype';
$user = 'srv105686_magicoprototype';
$pass = 'pMMdmYHXSBs6wM5LHNjd';
+$pass = 'pMMdmYHXSBs6wM5LHNjd';
$charset = 'utf8mb4';
+// Hasło do trybu komentowania
+$PROTOTYPE_PASSWORD = 'magic'; // Zmień to hasło!
+
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
${data.author || 'Anonim'}
+ ${data.created_at} +${data.comment}
+