commit 9e1957266cc01a00b328ee4ecd177731c19deaf4 Author: s4luorth Date: Sat Feb 7 13:06:39 2026 +0100 Initial commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c0abcb6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:github.com)", + "WebFetch(domain:pwr-solaar.github.io)", + "WebFetch(domain:lekensteyn.nl)" + ] + } +} diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..3f38a71 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,184 @@ +/** + * Door Status Voting - Admin Styles + */ + +.dsv-admin-wrap { + max-width: 1200px; +} + +.dsv-admin-wrap h1 { + font-size: 28px; + margin-bottom: 25px; +} + +/* Grid Layout */ +.dsv-admin-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +.dsv-admin-card { + background: white; + border: 1px solid #ccd0d4; + border-radius: 8px; + padding: 20px 25px; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); +} + +.dsv-admin-card h2 { + margin: 0 0 15px 0; + padding: 0 0 10px 0; + border-bottom: 1px solid #eee; + font-size: 16px; +} + +.dsv-admin-card-full { + grid-column: 1 / -1; +} + +/* Shortcode Display */ +.dsv-shortcode { + display: block; + background: #f0f0f1; + padding: 12px 15px; + border-radius: 4px; + font-size: 14px; + margin: 10px 0; + user-select: all; + cursor: pointer; +} + +.dsv-shortcode:hover { + background: #e5e5e5; +} + +/* Form Elements */ +#dsv-settings-form textarea { + width: 100%; + font-family: monospace; +} + +#dsv-settings-form .button { + margin-top: 10px; +} + +.dsv-save-status { + margin-left: 10px; + color: #00a32a; + font-weight: 500; +} + +/* Table Styles */ +.dsv-admin-card table { + margin-top: 10px; +} + +.dsv-admin-card table th, +.dsv-admin-card table td { + padding: 12px 10px; +} + +/* Badges */ +.dsv-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + color: white; +} + +.dsv-badge-open { + background: #4CAF50; +} + +.dsv-badge-closed { + background: #f44336; +} + +.dsv-badge-tied { + background: #FF9800; +} + +.dsv-meta-empty { + color: #999; +} + +/* Meta-Status Buttons */ +.dsv-meta-buttons { + margin-top: 5px; + display: flex; + gap: 3px; +} + +.dsv-meta-set-btn { + min-width: 28px; + padding: 2px 5px !important; + font-size: 12px !important; + line-height: 1.4 !important; + height: auto !important; +} + +.dsv-meta-set-btn.active { + box-shadow: inset 0 0 0 2px #2271b1; + background: #f0f6fc; +} + +/* Abweichung hervorheben */ +.dsv-mismatch { + background: #fff3cd !important; +} + +.dsv-mismatch td { + border-left-color: #FF9800; +} + +.dsv-mismatch td:first-child { + border-left: 3px solid #FF9800; +} + +/* Disabled Label */ +.dsv-disabled-label { + color: #d63638; + font-size: 12px; +} + +/* Action Buttons */ +.dsv-actions-cell { + min-width: 280px; +} + +.dsv-action-buttons { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.dsv-action-buttons .button { + font-size: 11px; + padding: 2px 8px; + height: auto; + line-height: 1.8; +} + +.dsv-reset-type-btn { + min-width: 85px; +} + +/* No Data */ +.dsv-no-data { + color: #666; + font-style: italic; + padding: 20px; + text-align: center; + background: #f9f9f9; + border-radius: 4px; +} + +/* Responsive */ +@media (max-width: 782px) { + .dsv-admin-grid { + grid-template-columns: 1fr; + } +} diff --git a/assets/css/frontend.css b/assets/css/frontend.css new file mode 100644 index 0000000..66064d8 --- /dev/null +++ b/assets/css/frontend.css @@ -0,0 +1,550 @@ +/* ============================================ + DOOR STATUS VOTING - FRONTEND STYLES + Angepasst an Website-Design mit Elementor-Variablen + ============================================ */ + +:root { + --dsv-primary: #2F4858; + --dsv-text: #141F27; + --dsv-accent: #C5422A; + --dsv-bg: #F6F9FC; + --dsv-white: #FFFFFF; + --dsv-hover: #B33822; + --dsv-border: #E0E5E9; + --dsv-success: #4CAF50; + --dsv-error: #C5422A; +} + +/* ============================================ + HAUPT-WIDGET + ============================================ */ + +.dsv-widget { + background: var(--dsv-bg); + border-radius: 12px; + padding: 24px 28px; + width: 100%; + border: 1px solid var(--dsv-border); + box-shadow: 0 4px 12px rgba(47, 72, 88, 0.06); + font-family: var(--e-global-typography-text-font-family), Sans-serif; + font-size: var(--e-global-typography-text-font-size); + font-weight: var(--e-global-typography-text-font-weight); + color: var(--e-global-color-text, var(--dsv-text)); + box-sizing: border-box; +} + +/* Titel */ +.dsv-title { + text-align: center; + font-size: 18px; + font-weight: 600; + color: var(--dsv-primary); + margin: 0 0 18px 0; +} + +/* Hauptzeile: Tür links, Voting rechts */ +.dsv-main-row { + display: flex; + align-items: center; + gap: 30px; +} + +/* ============================================ + TÜR CONTAINER + ============================================ */ + +.dsv-door-container { + position: relative; + width: 110px; + height: 150px; + flex-shrink: 0; + perspective: 500px; +} + +/* Status Badge */ +.dsv-status-badge { + position: absolute; + top: -10px; + left: 50%; + transform: translateX(-50%); + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 700; + color: white; + z-index: 10; + white-space: nowrap; + transition: all 0.5s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.dsv-status-open { + background: linear-gradient(135deg, var(--dsv-success), #43A047); + box-shadow: 0 3px 10px rgba(76, 175, 80, 0.35); +} + +.dsv-status-closed { + background: linear-gradient(135deg, var(--dsv-accent), var(--dsv-hover)); + box-shadow: 0 3px 10px rgba(197, 66, 42, 0.35); +} + +.dsv-status-tied { + background: linear-gradient(135deg, #FF9800, #F57C00); + box-shadow: 0 3px 10px rgba(255, 152, 0, 0.35); +} + +/* Türrahmen */ +.dsv-door-frame { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + width: 85px; + height: 120px; + background: linear-gradient(135deg, #5D4037 0%, #4E342E 100%); + border-radius: 4px 4px 0 0; + border: 4px solid #3E2723; + border-bottom: 6px solid #3E2723; + box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +/* Licht hinter der Tür */ +.dsv-light { + position: absolute; + inset: 3px; + border-radius: 2px; + background: linear-gradient(180deg, #FFF9C4 0%, #FFEE58 50%, #FFC107 100%); + opacity: 0; + transition: opacity 0.7s ease; +} + +.dsv-widget[data-status="open"] .dsv-light { + opacity: 1; + box-shadow: 0 0 35px 12px rgba(255, 235, 59, 0.3); +} + +.dsv-widget[data-status="tied"] .dsv-light { + opacity: 0.5; +} + +/* Lichtstrahlen */ +.dsv-light-rays { + position: absolute; + inset: 3px; + overflow: hidden; + border-radius: 2px; + opacity: 0; + transition: opacity 0.7s ease; +} + +.dsv-widget[data-status="open"] .dsv-light-rays, +.dsv-widget[data-status="tied"] .dsv-light-rays { + opacity: 1; +} + +.dsv-ray { + position: absolute; + width: 2px; + height: 100%; + background: rgba(255, 255, 200, 0.4); + filter: blur(1px); + transform: rotate(5deg); +} + +.dsv-ray:nth-child(1) { left: 15%; } +.dsv-ray:nth-child(2) { left: 30%; } +.dsv-ray:nth-child(3) { left: 45%; } +.dsv-ray:nth-child(4) { left: 60%; } +.dsv-ray:nth-child(5) { left: 75%; } + +/* ============================================ + DIE TÜR + ============================================ */ + +.dsv-door { + position: absolute; + top: 3px; + left: 3px; + width: 71px; + height: 106px; + border-radius: 2px 2px 0 0; + transform-origin: left center; + transform-style: preserve-3d; + transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); + + background: + linear-gradient(90deg, + rgba(0,0,0,0.1) 0%, + transparent 3%, + transparent 97%, + rgba(0,0,0,0.1) 100% + ), + linear-gradient(180deg, + #8D6E63 0%, + #795548 15%, + #8D6E63 30%, + #6D4C41 50%, + #795548 70%, + #8D6E63 85%, + #795548 100% + ); + + box-shadow: 2px 0 6px rgba(0, 0, 0, 0.3); +} + +.dsv-widget[data-status="open"] .dsv-door { + transform: rotateY(-70deg); + box-shadow: 4px 0 12px rgba(0, 0, 0, 0.4); +} + +.dsv-widget[data-status="tied"] .dsv-door { + transform: rotateY(-35deg); + box-shadow: 3px 0 8px rgba(0, 0, 0, 0.35); +} + +.dsv-widget[data-status="closed"] .dsv-door { + transform: rotateY(0deg); +} + +/* Holzmaserung */ +.dsv-door-grain { + position: absolute; + inset: 0; + border-radius: 2px 2px 0 0; + opacity: 0.15; + background-image: + repeating-linear-gradient( + 92deg, + transparent, + transparent 5px, + rgba(0,0,0,0.1) 5px, + rgba(0,0,0,0.1) 6px + ); +} + +/* Türpaneele */ +.dsv-door-panel { + position: absolute; + left: 6px; + right: 6px; + border: 2px solid rgba(62, 39, 35, 0.4); + border-radius: 2px; + background: rgba(93, 64, 55, 0.2); +} + +.dsv-door-panel-top { + top: 10px; + height: 32px; +} + +.dsv-door-panel-bottom { + bottom: 10px; + height: 42px; +} + +/* Türklinke */ +.dsv-door-handle { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); +} + +.dsv-handle-plate { + width: 9px; + height: 26px; + border-radius: 4px; + background: linear-gradient(135deg, #FFD54F 0%, #FFA000 50%, #FF8F00 100%); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); +} + +.dsv-handle-lever { + position: absolute; + top: 50%; + right: -1px; + transform: translateY(-50%); + width: 11px; + height: 5px; + border-radius: 0 2px 2px 0; + background: linear-gradient(180deg, #FFD54F 0%, #FFA000 100%); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); +} + +/* Schatten */ +.dsv-shadow { + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%); + width: 75px; + height: 10px; + background: radial-gradient(ellipse, rgba(0,0,0,0.2) 0%, transparent 70%); + transition: all 0.7s ease; +} + +.dsv-widget[data-status="open"] .dsv-shadow { + width: 90px; + transform: translateX(-50%) scaleX(1.2); +} + +/* ============================================ + VOTING SECTION (rechte Seite) + ============================================ */ + +.dsv-voting-section { + flex: 1; + min-width: 0; +} + +/* Fortschrittsbalken */ +.dsv-progress-container { + margin-bottom: 14px; +} + +.dsv-progress-bar { + position: relative; + height: 28px; + background: var(--dsv-bg); + border-radius: 14px; + overflow: hidden; + border: 1px solid var(--dsv-border); + display: flex; +} + +.dsv-progress-open { + height: 100%; + background: linear-gradient(90deg, var(--dsv-success) 0%, #66BB6A 100%); + display: flex; + align-items: center; + justify-content: flex-start; + padding-left: 12px; + transition: width 0.7s cubic-bezier(0.4, 0, 0.2, 1); +} + +.dsv-progress-closed { + height: 100%; + background: linear-gradient(90deg, #EF5350 0%, var(--dsv-accent) 100%); + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 12px; + transition: width 0.7s cubic-bezier(0.4, 0, 0.2, 1); +} + +.dsv-progress-count { + color: white; + font-size: 12px; + font-weight: 700; + text-shadow: 0 1px 2px rgba(0,0,0,0.2); +} + +.dsv-progress-divider { + position: absolute; + left: 50%; + top: 0; + width: 2px; + height: 100%; + background: rgba(255,255,255,0.5); + transform: translateX(-50%); +} + +.dsv-progress-labels { + display: flex; + justify-content: space-between; + margin-top: 6px; + font-size: 12px; + font-weight: var(--e-global-typography-text-font-weight); +} + +.dsv-label-open { + color: #388E3C; +} + +.dsv-label-closed { + color: var(--dsv-accent); +} + +/* ============================================ + VOTING BUTTONS + ============================================ */ + +.dsv-buttons { + display: flex; + gap: 12px; +} + +.dsv-buttons.dsv-voted { + justify-content: flex-start; +} + +.dsv-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 18px; + border: none; + border-radius: 8px; + font-family: var(--e-global-typography-text-font-family), Sans-serif; + font-size: 14px; + font-weight: 600; + color: white; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 3px 10px rgba(0,0,0,0.15); +} + +.dsv-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0,0,0,0.2); +} + +.dsv-btn:active { + transform: translateY(0); +} + +.dsv-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.dsv-btn-open { + background: linear-gradient(135deg, var(--dsv-success) 0%, #388E3C 100%); +} + +.dsv-btn-open:hover { + box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); +} + +.dsv-btn-closed { + background: linear-gradient(135deg, var(--dsv-accent) 0%, var(--dsv-hover) 100%); +} + +.dsv-btn-closed:hover { + box-shadow: 0 5px 15px rgba(197, 66, 42, 0.4); +} + +.dsv-btn-icon { + font-size: 16px; +} + +.dsv-btn-text { + font-size: 14px; +} + +/* Voted Message mit Remove Button */ +.dsv-voted-message { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + border: 1px solid; +} + +.dsv-voted-open { + background: #E8F5E9; + color: #2E7D32; + border-color: #A5D6A7; +} + +.dsv-voted-closed { + background: #FFEBEE; + color: var(--dsv-hover); + border-color: #FFCDD2; +} + +.dsv-voted-icon { + font-size: 16px; +} + +/* Remove Vote Button */ +.dsv-remove-vote { + background: transparent; + border: none; + color: inherit; + opacity: 0.5; + cursor: pointer; + padding: 4px 8px; + margin-left: 6px; + border-radius: 4px; + font-size: 14px; + transition: all 0.2s; +} + +.dsv-remove-vote:hover { + opacity: 1; + background: rgba(0,0,0,0.08); +} + +/* ============================================ + INFO TEXT + ============================================ */ + +.dsv-info { + color: #666; + font-size: 12px; + margin: 12px 0 0 0; + font-weight: var(--e-global-typography-text-font-weight); +} + +.dsv-total { + font-weight: 600; + color: var(--dsv-primary); +} + +/* ============================================ + ANIMATIONEN + ============================================ */ + +.dsv-widget.dsv-animating .dsv-door { + transition: transform 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55); +} + +@keyframes dsv-pulse { + 0% { transform: translateX(-50%) scale(1); } + 50% { transform: translateX(-50%) scale(1.08); } + 100% { transform: translateX(-50%) scale(1); } +} + +.dsv-widget.dsv-just-voted .dsv-status-badge { + animation: dsv-pulse 0.5s ease; +} + +/* ============================================ + RESPONSIVE + ============================================ */ + +@media (max-width: 550px) { + .dsv-widget { + padding: 20px; + } + + .dsv-main-row { + flex-direction: column; + gap: 20px; + } + + .dsv-door-container { + transform: scale(0.95); + } + + .dsv-voting-section { + width: 100%; + } + + .dsv-btn { + padding: 11px 14px; + font-size: 13px; + } + + .dsv-title { + font-size: 16px; + } +} diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..4374a02 --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,259 @@ +/** + * Door Status Voting - Admin JavaScript + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + initSettingsForm(); + initResetButtons(); + initResetByTypeButtons(); + initMismatchFilter(); + initMetaStatusButtons(); + }); + + /** + * Settings Form Handler + */ + function initSettingsForm() { + $('#dsv-settings-form').on('submit', function(e) { + e.preventDefault(); + + var $form = $(this); + var $btn = $form.find('.button-primary'); + var $status = $form.find('.dsv-save-status'); + + $btn.prop('disabled', true).text('Speichern...'); + $status.text(''); + + $.ajax({ + type: 'POST', + url: dsvAdmin.ajaxUrl, + data: { + action: 'dsv_save_settings', + nonce: dsvAdmin.nonce, + disabled_posts: $('#dsv-disabled-posts').val() + }, + success: function(response) { + if (response.success) { + $status.text('✓ Gespeichert').css('color', '#00a32a'); + } else { + $status.text('✗ Fehler: ' + (response.data?.message || 'Unbekannt')).css('color', '#d63638'); + } + }, + error: function() { + $status.text('✗ Verbindungsfehler').css('color', '#d63638'); + }, + complete: function() { + $btn.prop('disabled', false).text('Einstellungen speichern'); + + setTimeout(function() { + $status.fadeOut(300, function() { + $(this).text('').show(); + }); + }, 3000); + } + }); + }); + } + + /** + * Reset All Buttons Handler + */ + function initResetButtons() { + $(document).on('click', '.dsv-reset-btn', function() { + var $btn = $(this); + var $row = $btn.closest('tr'); + var postId = $btn.data('post-id'); + + if (!confirm('Alle Votes für diesen Beitrag wirklich zurücksetzen?')) { + return; + } + + $btn.prop('disabled', true).text('Löschen...'); + + $.ajax({ + type: 'POST', + url: dsvAdmin.ajaxUrl, + data: { + action: 'dsv_reset_votes', + nonce: dsvAdmin.nonce, + post_id: postId + }, + success: function(response) { + if (response.success) { + $row.fadeOut(300, function() { + $(this).remove(); + checkEmptyTable(); + }); + } else { + alert('Fehler: ' + (response.data?.message || 'Unbekannt')); + $btn.prop('disabled', false).text('Alle löschen'); + } + }, + error: function() { + alert('Verbindungsfehler'); + $btn.prop('disabled', false).text('Alle löschen'); + } + }); + }); + } + + /** + * Reset By Type Buttons Handler + */ + function initResetByTypeButtons() { + $(document).on('click', '.dsv-reset-type-btn', function() { + var $btn = $(this); + var $row = $btn.closest('tr'); + var postId = $btn.data('post-id'); + var type = $btn.data('type'); + var typeLabel = type === 'open' ? 'Geöffnet' : 'Geschlossen'; + + if (!confirm('Alle "' + typeLabel + '" Stimmen für diesen Beitrag löschen?')) { + return; + } + + var originalText = $btn.text(); + $btn.prop('disabled', true).text('...'); + + $.ajax({ + type: 'POST', + url: dsvAdmin.ajaxUrl, + data: { + action: 'dsv_reset_votes_by_type', + nonce: dsvAdmin.nonce, + post_id: postId, + type: type + }, + success: function(response) { + if (response.success) { + // Zähler aktualisieren + $row.find('.dsv-count-open').text(response.data.votes.open); + $row.find('.dsv-count-closed').text(response.data.votes.closed); + $row.find('.dsv-count-total').text(response.data.votes.open + response.data.votes.closed); + + // Status Badge aktualisieren + var $badge = $row.find('.dsv-badge'); + $badge.removeClass('dsv-badge-open dsv-badge-closed dsv-badge-tied'); + + if (response.data.votes.open > response.data.votes.closed) { + $badge.addClass('dsv-badge-open').text('Geöffnet'); + } else if (response.data.votes.closed > response.data.votes.open) { + $badge.addClass('dsv-badge-closed').text('Geschlossen'); + } else { + $badge.addClass('dsv-badge-tied').text('Unklar'); + } + + // Wenn keine Votes mehr, Zeile entfernen + if (response.data.votes.open === 0 && response.data.votes.closed === 0) { + $row.fadeOut(300, function() { + $(this).remove(); + checkEmptyTable(); + }); + } else { + // Kurzes Feedback + $btn.text('✓').css('color', '#00a32a'); + setTimeout(function() { + $btn.prop('disabled', false).text(originalText).css('color', ''); + }, 1000); + } + } else { + alert('Fehler: ' + (response.data?.message || 'Unbekannt')); + $btn.prop('disabled', false).text(originalText); + } + }, + error: function() { + alert('Verbindungsfehler'); + $btn.prop('disabled', false).text(originalText); + } + }); + }); + } + + /** + * Filter: Nur Abweichungen anzeigen + */ + function initMismatchFilter() { + var $btn = $('#dsv-filter-mismatch'); + var active = false; + + $btn.on('click', function() { + active = !active; + var $table = $('#dsv-votes-table'); + + if (active) { + $table.find('tbody tr').not('.dsv-mismatch').hide(); + $btn.addClass('button-primary').text('Alle anzeigen'); + } else { + $table.find('tbody tr').show(); + $btn.removeClass('button-primary').text('Nur Abweichungen anzeigen'); + } + }); + } + + /** + * Meta-Status per Knopfdruck ändern + */ + function initMetaStatusButtons() { + $(document).on('click', '.dsv-meta-set-btn', function() { + var $btn = $(this); + var $cell = $btn.closest('.dsv-meta-status-cell'); + var postId = $btn.data('post-id'); + var status = $btn.data('status'); + + $cell.find('.dsv-meta-set-btn').prop('disabled', true); + + $.ajax({ + type: 'POST', + url: dsvAdmin.ajaxUrl, + data: { + action: 'dsv_update_meta_status', + nonce: dsvAdmin.nonce, + post_id: postId, + status: status + }, + success: function(response) { + if (response.success) { + // Badge aktualisieren + var badgeClass = 'dsv-badge-tied'; + var lower = status.toLowerCase(); + if (lower === 'geöffnet') badgeClass = 'dsv-badge-open'; + else if (lower === 'geschlossen') badgeClass = 'dsv-badge-closed'; + + var $badge = $cell.find('.dsv-badge, .dsv-meta-empty'); + if ($badge.hasClass('dsv-meta-empty')) { + $badge.removeClass('dsv-meta-empty').addClass('dsv-badge'); + } + $badge.removeClass('dsv-badge-open dsv-badge-closed dsv-badge-tied') + .addClass(badgeClass) + .text(status); + + // Active-State der Buttons + $cell.find('.dsv-meta-set-btn').removeClass('active'); + $btn.addClass('active'); + } else { + alert('Fehler: ' + (response.data?.message || 'Unbekannt')); + } + }, + error: function() { + alert('Verbindungsfehler'); + }, + complete: function() { + $cell.find('.dsv-meta-set-btn').prop('disabled', false); + } + }); + }); + } + + /** + * Prüfen ob Tabelle leer ist + */ + function checkEmptyTable() { + if ($('tbody tr').length === 0) { + location.reload(); + } + } + +})(jQuery); diff --git a/assets/js/frontend.js b/assets/js/frontend.js new file mode 100644 index 0000000..394eeec --- /dev/null +++ b/assets/js/frontend.js @@ -0,0 +1,257 @@ +/** + * Door Status Voting - Frontend JavaScript + */ + +(function() { + 'use strict'; + + document.addEventListener('DOMContentLoaded', function() { + initDoorVoting(); + }); + + function initDoorVoting() { + const widgets = document.querySelectorAll('.dsv-widget'); + + widgets.forEach(function(widget) { + // Vote Buttons + const buttons = widget.querySelectorAll('.dsv-btn'); + buttons.forEach(function(btn) { + btn.addEventListener('click', function() { + handleVote(widget, btn.dataset.vote); + }); + }); + + // Remove Vote Button + const removeBtn = widget.querySelector('.dsv-remove-vote'); + if (removeBtn) { + removeBtn.addEventListener('click', function() { + handleRemoveVote(widget); + }); + } + }); + } + + function handleVote(widget, vote) { + const postId = widget.dataset.postId; + const hasVoted = widget.dataset.hasVoted === 'true'; + + if (hasVoted) return; + + // Buttons deaktivieren + const buttons = widget.querySelectorAll('.dsv-btn'); + buttons.forEach(function(btn) { + btn.disabled = true; + }); + + // Animation starten + widget.classList.add('dsv-animating'); + + // AJAX Request + const formData = new FormData(); + formData.append('action', 'dsv_vote'); + formData.append('post_id', postId); + formData.append('vote', vote); + formData.append('nonce', dsvConfig.nonce); + + fetch(dsvConfig.ajaxUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin' + }) + .then(function(response) { + return response.json(); + }) + .then(function(data) { + if (data.success) { + updateWidget(widget, data.data, vote); + } else { + console.error('Vote error:', data.data?.message); + buttons.forEach(function(btn) { + btn.disabled = false; + }); + } + }) + .catch(function(error) { + console.error('Network error:', error); + buttons.forEach(function(btn) { + btn.disabled = false; + }); + }) + .finally(function() { + widget.classList.remove('dsv-animating'); + }); + } + + function handleRemoveVote(widget) { + const postId = widget.dataset.postId; + + // Button deaktivieren + const removeBtn = widget.querySelector('.dsv-remove-vote'); + if (removeBtn) { + removeBtn.disabled = true; + removeBtn.textContent = '...'; + } + + widget.classList.add('dsv-animating'); + + const formData = new FormData(); + formData.append('action', 'dsv_remove_vote'); + formData.append('post_id', postId); + formData.append('nonce', dsvConfig.nonce); + + fetch(dsvConfig.ajaxUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin' + }) + .then(function(response) { + return response.json(); + }) + .then(function(data) { + if (data.success) { + resetWidgetToVoting(widget, data.data); + } else { + console.error('Remove vote error:', data.data?.message); + if (removeBtn) { + removeBtn.disabled = false; + removeBtn.textContent = '✕'; + } + } + }) + .catch(function(error) { + console.error('Network error:', error); + if (removeBtn) { + removeBtn.disabled = false; + removeBtn.textContent = '✕'; + } + }) + .finally(function() { + widget.classList.remove('dsv-animating'); + }); + } + + function updateWidget(widget, data, userVote) { + // Status aktualisieren + widget.dataset.status = data.status; + widget.dataset.hasVoted = 'true'; + widget.dataset.userVote = userVote; + + // Badge aktualisieren + const badge = widget.querySelector('.dsv-status-badge'); + badge.className = 'dsv-status-badge dsv-status-' + data.status; + + if (data.status === 'open') { + badge.innerHTML = '🟢 GEÖFFNET'; + } else if (data.status === 'closed') { + badge.innerHTML = '🔴 GESCHLOSSEN'; + } else { + badge.innerHTML = '🟡 UNKLAR'; + } + + // Fortschrittsbalken aktualisieren + updateProgressBar(widget, data); + + // Buttons durch Bestätigung ersetzen + const buttonsContainer = widget.querySelector('.dsv-buttons'); + buttonsContainer.classList.add('dsv-voted'); + + const voteClass = userVote === 'open' ? 'dsv-voted-open' : 'dsv-voted-closed'; + const voteIcon = userVote === 'open' ? '✓' : '✗'; + const voteText = userVote === 'open' ? 'Geöffnet' : 'Geschlossen'; + + buttonsContainer.innerHTML = + '
' + + '' + voteIcon + '' + + 'Du hast für "' + voteText + '" gestimmt' + + '' + + '
'; + + // Event Listener für neuen Remove Button + const newRemoveBtn = buttonsContainer.querySelector('.dsv-remove-vote'); + newRemoveBtn.addEventListener('click', function() { + handleRemoveVote(widget); + }); + + // Total aktualisieren + widget.querySelector('.dsv-total').textContent = data.total; + + // Pulse Animation + widget.classList.add('dsv-just-voted'); + setTimeout(function() { + widget.classList.remove('dsv-just-voted'); + }, 500); + } + + function resetWidgetToVoting(widget, data) { + // Status aktualisieren + widget.dataset.status = data.status; + widget.dataset.hasVoted = 'false'; + widget.dataset.userVote = ''; + + // Badge aktualisieren + const badge = widget.querySelector('.dsv-status-badge'); + badge.className = 'dsv-status-badge dsv-status-' + data.status; + + if (data.status === 'open') { + badge.innerHTML = '🟢 GEÖFFNET'; + } else if (data.status === 'closed') { + badge.innerHTML = '🔴 GESCHLOSSEN'; + } else { + badge.innerHTML = '🟡 UNKLAR'; + } + + // Fortschrittsbalken aktualisieren + updateProgressBar(widget, data); + + // Voting Buttons wieder anzeigen + const buttonsContainer = widget.querySelector('.dsv-buttons'); + buttonsContainer.classList.remove('dsv-voted'); + + buttonsContainer.innerHTML = + '' + + ''; + + // Event Listener für neue Buttons + const newButtons = buttonsContainer.querySelectorAll('.dsv-btn'); + newButtons.forEach(function(btn) { + btn.addEventListener('click', function() { + handleVote(widget, btn.dataset.vote); + }); + }); + + // Total aktualisieren + widget.querySelector('.dsv-total').textContent = data.total; + } + + function updateProgressBar(widget, data) { + const progressOpen = widget.querySelector('.dsv-progress-open'); + const progressClosed = widget.querySelector('.dsv-progress-closed'); + + progressOpen.style.width = data.openPercent + '%'; + progressClosed.style.width = (100 - data.openPercent) + '%'; + + // Zähler + if (data.openPercent > 15) { + progressOpen.innerHTML = '' + data.votes.open + ''; + } else { + progressOpen.innerHTML = ''; + } + + if ((100 - data.openPercent) > 15) { + progressClosed.innerHTML = '' + data.votes.closed + ''; + } else { + progressClosed.innerHTML = ''; + } + + // Labels + widget.querySelector('.dsv-label-open').textContent = 'Geöffnet (' + data.openPercent + '%)'; + widget.querySelector('.dsv-label-closed').textContent = 'Geschlossen (' + (100 - data.openPercent) + '%)'; + } + +})(); diff --git a/door-status-voting.php b/door-status-voting.php new file mode 100644 index 0000000..900b2cf --- /dev/null +++ b/door-status-voting.php @@ -0,0 +1,735 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('dsv_nonce') + ]); + } + + /** + * Shortcode rendern + */ + public function render_shortcode($atts) { + $atts = shortcode_atts([ + 'post_id' => get_the_ID() + ], $atts); + + $post_id = intval($atts['post_id']); + + // Prüfen ob für diesen Post deaktiviert + $disabled_posts = get_option('dsv_disabled_posts', []); + if (in_array($post_id, $disabled_posts)) { + return ''; + } + + // Votes laden + $votes = $this->get_votes($post_id); + $total = $votes['open'] + $votes['closed']; + $open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50; + + // Status ermitteln + if ($votes['open'] > $votes['closed']) { + $status = 'open'; + $status_text = 'GEÖFFNET'; + $status_icon = '🟢'; + } elseif ($votes['closed'] > $votes['open']) { + $status = 'closed'; + $status_text = 'GESCHLOSSEN'; + $status_icon = '🔴'; + } else { + $status = 'tied'; + $status_text = 'UNKLAR'; + $status_icon = '🟡'; + } + + // User Vote aus Cookie prüfen + $cookie_name = 'dsv_vote_' . $post_id; + $user_vote = isset($_COOKIE[$cookie_name]) ? sanitize_text_field($_COOKIE[$cookie_name]) : null; + $has_voted = !empty($user_vote); + + ob_start(); + ?> +
+ +

Ist das Restaurant noch geöffnet?

+ +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+ 15): ?> + + +
+
+ 15): ?> + + +
+
+
+
+ Geöffnet (%) + Geschlossen (%) +
+
+ + +
+ + + + +
+ + + Du hast für "" gestimmt + + +
+ +
+ + +

+ Besucher haben abgestimmt +

+
+
+
+ 0, 'closed' => 0]; + } + + $open = $closed = 0; + foreach ($data as $vote) { + if ($vote === 'open') $open++; + if ($vote === 'closed') $closed++; + } + return ['open' => $open, 'closed' => $closed]; + } + + /** + * User-Key generieren (IP Hash) + */ + private function get_user_key() { + $ip = ''; + if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) { + $ip = $_SERVER['HTTP_CF_CONNECTING_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; + } else { + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + } + return md5($ip . wp_salt()); + } + + /** + * AJAX: Vote speichern + */ + public function handle_vote() { + check_ajax_referer('dsv_nonce', 'nonce'); + + $post_id = intval($_POST['post_id'] ?? 0); + $vote = sanitize_text_field($_POST['vote'] ?? ''); + + if (!$post_id || !in_array($vote, ['open', 'closed'])) { + wp_send_json_error(['message' => 'Ungültige Anfrage']); + } + + // Prüfen ob deaktiviert + $disabled_posts = get_option('dsv_disabled_posts', []); + if (in_array($post_id, $disabled_posts)) { + wp_send_json_error(['message' => 'Voting deaktiviert']); + } + + $user_key = $this->get_user_key(); + $data = get_post_meta($post_id, '_dsv_votes', true); + if (!is_array($data)) $data = []; + + // Bereits gevotet? (Server-side Check) + if (isset($data[$user_key])) { + wp_send_json_error(['message' => 'Bereits abgestimmt']); + } + + // Vote speichern + $data[$user_key] = $vote; + update_post_meta($post_id, '_dsv_votes', $data); + + // Cookie setzen (30 Tage) + $cookie_name = 'dsv_vote_' . $post_id; + setcookie($cookie_name, $vote, time() + (30 * DAY_IN_SECONDS), COOKIEPATH, COOKIE_DOMAIN); + + // Neue Votes zurückgeben + $votes = $this->get_votes($post_id); + $total = $votes['open'] + $votes['closed']; + $open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50; + + wp_send_json_success([ + 'votes' => $votes, + 'total' => $total, + 'openPercent' => $open_percent, + 'status' => $votes['open'] > $votes['closed'] ? 'open' : + ($votes['closed'] > $votes['open'] ? 'closed' : 'tied') + ]); + } + + /** + * AJAX: Aktuellen Status abrufen + */ + public function get_status() { + $post_id = intval($_GET['post_id'] ?? 0); + + if (!$post_id) { + wp_send_json_error(['message' => 'Ungültige Post ID']); + } + + $votes = $this->get_votes($post_id); + $total = $votes['open'] + $votes['closed']; + $open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50; + + wp_send_json_success([ + 'votes' => $votes, + 'total' => $total, + 'openPercent' => $open_percent, + 'status' => $votes['open'] > $votes['closed'] ? 'open' : + ($votes['closed'] > $votes['open'] ? 'closed' : 'tied') + ]); + } + + /** + * AJAX: Vote entfernen + */ + public function handle_remove_vote() { + check_ajax_referer('dsv_nonce', 'nonce'); + + $post_id = intval($_POST['post_id'] ?? 0); + + if (!$post_id) { + wp_send_json_error(['message' => 'Ungültige Anfrage']); + } + + $user_key = $this->get_user_key(); + $data = get_post_meta($post_id, '_dsv_votes', true); + + if (!is_array($data) || !isset($data[$user_key])) { + wp_send_json_error(['message' => 'Kein Vote gefunden']); + } + + // Vote entfernen + unset($data[$user_key]); + update_post_meta($post_id, '_dsv_votes', $data); + + // Cookie löschen + $cookie_name = 'dsv_vote_' . $post_id; + setcookie($cookie_name, '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN); + + // Neue Votes zurückgeben + $votes = $this->get_votes($post_id); + $total = $votes['open'] + $votes['closed']; + $open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50; + + wp_send_json_success([ + 'votes' => $votes, + 'total' => $total, + 'openPercent' => $open_percent, + 'status' => $votes['open'] > $votes['closed'] ? 'open' : + ($votes['closed'] > $votes['open'] ? 'closed' : 'tied') + ]); + } + + // ========================================================================= + // ADMIN + // ========================================================================= + + /** + * Admin Menü unter Werkzeuge + */ + public function add_admin_menu() { + add_management_page( + 'Tür-Status Voting', + 'Tür-Status', + 'manage_options', + 'door-status-settings', + [$this, 'render_admin_page'] + ); + } + + /** + * Admin Assets laden + */ + public function enqueue_admin_assets($hook) { + if ($hook !== 'tools_page_door-status-settings') { + return; + } + + wp_enqueue_style( + 'dsv-admin', + DSV_PLUGIN_URL . 'assets/css/admin.css', + [], + DSV_VERSION + ); + + wp_enqueue_script( + 'dsv-admin', + DSV_PLUGIN_URL . 'assets/js/admin.js', + ['jquery'], + DSV_VERSION, + true + ); + + wp_localize_script('dsv-admin', 'dsvAdmin', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('dsv_admin_nonce') + ]); + } + + /** + * Admin Seite rendern + */ + public function render_admin_page() { + if (!current_user_can('manage_options')) { + wp_die('Keine Berechtigung'); + } + + $disabled_posts = get_option('dsv_disabled_posts', []); + + // Alle Posts mit Votes holen + global $wpdb; + $posts_with_votes = $wpdb->get_col( + "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_dsv_votes'" + ); + + ?> +
+

🚪 Tür-Status Voting

+ +
+ + +
+

Shortcode

+

Füge das Voting-Widget mit folgendem Shortcode ein:

+ [door_status] +

+ Der Shortcode verwendet automatisch die aktuelle Post-ID. + Du kannst ihn in Posts, Seiten oder Page-Builder einfügen. +

+
+ + +
+

Voting deaktivieren

+

Gib Post-IDs ein, für die das Voting deaktiviert werden soll (kommagetrennt):

+
+ +

+ Der Shortcode wird auf diesen Seiten nichts ausgeben. +

+

+ + +

+
+
+ + +
+

Voting Übersicht

+ + +

Noch keine Votes vorhanden.

+ +

+ +

+ + + + + + + + + + + + + + get_votes($pid); + $total = $votes['open'] + $votes['closed']; + $is_disabled = in_array($pid, $disabled_posts); + + if ($votes['open'] > $votes['closed']) { + $status_badge = 'Geöffnet'; + } elseif ($votes['closed'] > $votes['open']) { + $status_badge = 'Geschlossen'; + } else { + $status_badge = 'Unklar'; + } + + // JetEngine Meta-Feld "status" auslesen + $meta_status = get_post_meta($pid, 'status', true); + $meta_status_lower = mb_strtolower(trim($meta_status)); + if (in_array($meta_status_lower, ['geöffnet', 'geoeffnet', 'open', 'offen'])) { + $meta_badge_class = 'dsv-badge-open'; + $meta_voting_match = ($votes['open'] > $votes['closed']); + } elseif (in_array($meta_status_lower, ['geschlossen', 'closed', 'zu'])) { + $meta_badge_class = 'dsv-badge-closed'; + $meta_voting_match = ($votes['closed'] > $votes['open']); + } elseif (in_array($meta_status_lower, ['neuer betreiber'])) { + $meta_badge_class = 'dsv-badge-tied'; + $meta_voting_match = false; + } elseif (!empty($meta_status)) { + $meta_badge_class = 'dsv-badge-tied'; + $meta_voting_match = false; + } else { + $meta_badge_class = ''; + $meta_voting_match = true; // kein Meta = kein Abweichung + } + + if (!empty($meta_status)) { + $meta_status_display = '' . esc_html($meta_status) . ''; + } else { + $meta_status_display = ''; + } + + $row_mismatch = !$meta_voting_match; + ?> + class="dsv-mismatch"> + + + + + + + + + + +
PostMeta-StatusVoting-StatusGeöffnetGeschlossenGesamtAktionen
+ post_title); ?> +
+ ID: + +
Voting deaktiviert + +
+ +
+ + +
+
+
+ + + + + Ansehen + +
+
+ +
+ +
+
+ 'Keine Berechtigung']); + } + + $disabled_posts_raw = sanitize_text_field($_POST['disabled_posts'] ?? ''); + $disabled_posts = []; + + if (!empty($disabled_posts_raw)) { + $ids = explode(',', $disabled_posts_raw); + foreach ($ids as $id) { + $id = intval(trim($id)); + if ($id > 0) { + $disabled_posts[] = $id; + } + } + } + + update_option('dsv_disabled_posts', $disabled_posts); + + wp_send_json_success(['message' => 'Gespeichert']); + } + + /** + * AJAX: Votes zurücksetzen + */ + public function reset_votes() { + check_ajax_referer('dsv_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Keine Berechtigung']); + } + + $post_id = intval($_POST['post_id'] ?? 0); + + if (!$post_id) { + wp_send_json_error(['message' => 'Ungültige Post ID']); + } + + delete_post_meta($post_id, '_dsv_votes'); + + wp_send_json_success(['message' => 'Votes zurückgesetzt']); + } + + /** + * AJAX: Votes nach Typ zurücksetzen (nur open oder nur closed) + */ + public function reset_votes_by_type() { + check_ajax_referer('dsv_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Keine Berechtigung']); + } + + $post_id = intval($_POST['post_id'] ?? 0); + $type = sanitize_text_field($_POST['type'] ?? ''); + + if (!$post_id || !in_array($type, ['open', 'closed'])) { + wp_send_json_error(['message' => 'Ungültige Parameter']); + } + + $data = get_post_meta($post_id, '_dsv_votes', true); + if (!is_array($data)) { + wp_send_json_error(['message' => 'Keine Votes vorhanden']); + } + + // Nur Votes des angegebenen Typs entfernen + $removed = 0; + foreach ($data as $key => $vote) { + if ($vote === $type) { + unset($data[$key]); + $removed++; + } + } + + update_post_meta($post_id, '_dsv_votes', $data); + + // Neue Zahlen berechnen + $votes = $this->get_votes($post_id); + + wp_send_json_success([ + 'message' => $removed . ' Stimmen gelöscht', + 'votes' => $votes, + 'removed' => $removed + ]); + } + + /** + * AJAX: Meta-Status ändern + */ + public function update_meta_status() { + check_ajax_referer('dsv_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Keine Berechtigung']); + } + + $post_id = intval($_POST['post_id'] ?? 0); + $status = sanitize_text_field($_POST['status'] ?? ''); + + if (!$post_id || !in_array($status, ['geöffnet', 'geschlossen'])) { + wp_send_json_error(['message' => 'Ungültige Parameter']); + } + + update_post_meta($post_id, 'status', $status); + + wp_send_json_success(['message' => 'Status aktualisiert', 'status' => $status]); + } +} + +// Plugin initialisieren +DoorStatusVoting::get_instance(); + +// Aktivierungs-Hook +register_activation_hook(__FILE__, function() { + add_option('dsv_disabled_posts', []); +}); + +// Deaktivierungs-Hook +register_deactivation_hook(__FILE__, function() { + // Optional: Daten behalten oder löschen +}); diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..244f72e --- /dev/null +++ b/readme.txt @@ -0,0 +1,49 @@ +=== Restaurant Tür-Status === +Contributors: developer +Tags: voting, restaurant, status, door, animation +Requires at least: 5.0 +Tested up to: 6.4 +Stable tag: 1.0.0 +License: GPLv2 or later + +Interaktives Voting-Widget mit animierter Tür - Besucher können abstimmen ob ein Restaurant geöffnet oder geschlossen ist. + +== Beschreibung == + +Das Plugin zeigt eine animierte Holztür, die sich je nach Abstimmungsergebnis öffnet oder schließt. Besucher können mit einem Klick angeben, ob ein Restaurant noch geöffnet oder bereits geschlossen ist. + +**Features:** + +* Animierte 3D-Holztür mit Lichteffekten +* Einfaches Voting mit einem Klick +* Fortschrittsbalken zeigt Abstimmungsverhältnis +* Cookie-basierte Duplikat-Verhinderung +* Admin-Bereich unter Werkzeuge +* Voting für einzelne Posts deaktivierbar +* Responsive Design + +== Installation == + +1. Plugin-Ordner nach `/wp-content/plugins/` hochladen +2. Plugin im WordPress Admin aktivieren +3. Shortcode `[door_status]` in Posts/Seiten einfügen + +== Verwendung == + +**Shortcode:** +``` +[door_status] +``` + +Der Shortcode verwendet automatisch die aktuelle Post-ID. + +**Admin-Bereich:** +Unter *Werkzeuge > Tür-Status* findest du: +* Übersicht aller Posts mit Votes +* Möglichkeit, Voting für bestimmte Posts zu deaktivieren +* Button zum Zurücksetzen von Votes + +== Changelog == + += 1.0.0 = +* Erste Version