Initial plugin commit

This commit is contained in:
2026-04-13 21:01:07 +02:00
commit 4367aef84a
254 changed files with 104260 additions and 0 deletions

302
assets/css/admin.css Normal file
View File

@@ -0,0 +1,302 @@
.kgvvm-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin: 18px 0 24px;
}
.kgvvm-card {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 8px;
padding: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.kgvvm-card h2,
.kgvvm-card h3 {
margin-top: 0;
}
.kgvvm-toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
margin: 16px 0;
}
.kgvvm-filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.kgvvm-form-table th {
width: 220px;
}
.kgvvm-help {
color: #50575e;
font-size: 12px;
}
.kgvvm-status {
display: inline-block;
padding: 3px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.kgvvm-status--green {
background: #edf7ed;
color: #0a5c1a;
}
.kgvvm-status--orange {
background: #fff4e5;
color: #8a4b00;
}
.kgvvm-status--gray {
background: #f1f1f1;
color: #444;
}
.kgvvm-status--blue {
background: #eef5ff;
color: #1d4f91;
}
.kgvvm-multiselect {
min-width: 320px;
min-height: 140px;
}
.kgvvm-open-swap-modal {
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
}
.kgvvm-modal[hidden] {
display: none !important;
}
.kgvvm-modal {
position: fixed;
inset: 0;
z-index: 100000;
}
.kgvvm-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
}
.kgvvm-modal__dialog {
position: relative;
z-index: 1;
width: min(760px, calc(100vw - 32px));
max-height: calc(100vh - 48px);
overflow: auto;
margin: 24px auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.kgvvm-modal__close {
position: absolute;
top: 12px;
right: 12px;
border: 0;
background: transparent;
font-size: 24px;
line-height: 1;
cursor: pointer;
color: #50575e;
}
.kgvvm-modal__summary {
margin: 16px 0;
}
.kgvvm-modal__summary p {
margin: 0 0 6px;
}
.kgvvm-modal__actions {
display: flex;
gap: 8px;
align-items: center;
}
.kgvvm-print-page {
max-width: 1100px;
}
.kgvvm-print-actions {
display: flex;
gap: 8px;
margin: 12px 0 18px;
}
.kgvvm-print-page ul {
margin: 0;
}
.kgvvm-print-page li {
margin-bottom: 6px;
}
.kgvvm-chat-app {
display: grid;
grid-template-columns: minmax(180px, 220px) 1fr;
gap: 16px;
margin-top: 16px;
}
.kgvvm-chat-room-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.kgvvm-chat-room {
width: 100%;
justify-content: flex-start;
}
.kgvvm-chat-room.is-active {
background: #2271b1;
border-color: #2271b1;
color: #fff;
}
.kgvvm-chat-panel__header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 12px;
}
.kgvvm-chat-messages {
background: #f6f7f7;
border: 1px solid #dcdcde;
border-radius: 8px;
min-height: 320px;
max-height: 520px;
overflow-y: auto;
padding: 12px;
}
.kgvvm-chat-empty {
margin: 0;
color: #50575e;
}
.kgvvm-chat-message {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 10px;
padding: 10px 12px;
margin-bottom: 10px;
}
.kgvvm-chat-message--own {
border-color: #72aee6;
background: #eef6ff;
}
.kgvvm-chat-message__meta {
color: #50575e;
font-size: 12px;
margin-bottom: 6px;
}
.kgvvm-chat-message__body {
white-space: normal;
word-break: break-word;
}
.kgvvm-chat-form {
margin-top: 12px;
}
.kgvvm-chat-form textarea {
width: 100%;
resize: vertical;
}
.kgvvm-chat-form__actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.kgvvm-chat-form__actions .is-error {
color: #b32d2e;
}
@media (max-width: 782px) {
.kgvvm-chat-app {
grid-template-columns: 1fr;
}
.kgvvm-chat-panel__header,
.kgvvm-chat-form__actions {
flex-direction: column;
align-items: stretch;
}
}
@media print {
#wpadminbar,
#adminmenumain,
#screen-meta,
#screen-meta-links,
.notice,
.update-nag,
.kgvvm-print-actions,
.page-title-action {
display: none !important;
}
#wpcontent,
#wpfooter {
margin-left: 0 !important;
}
.wrap,
.kgvvm-print-page {
margin: 0 !important;
padding: 0 !important;
max-width: 100% !important;
}
.kgvvm-card {
box-shadow: none;
border-color: #d0d0d0;
break-inside: avoid;
}
table {
break-inside: auto;
}
tr,
td,
th {
break-inside: avoid;
}
}

262
assets/js/chat.js Normal file
View File

@@ -0,0 +1,262 @@
(function () {
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function renderMessage(message, currentUserId) {
var ownClass = Number(message.user_id) === Number(currentUserId) ? ' kgvvm-chat-message--own' : '';
var roleLabel = message.role ? ' · ' + escapeHtml(message.role) : '';
var text = escapeHtml(message.message).replace(/\n/g, '<br>');
return '' +
'<article class="kgvvm-chat-message' + ownClass + '" data-id="' + escapeHtml(message.id) + '">' +
'<div class="kgvvm-chat-message__meta">' +
'<strong>' + escapeHtml(message.user) + '</strong>' + roleLabel + ' · ' + escapeHtml(message.time) +
'</div>' +
'<div class="kgvvm-chat-message__body">' + text + '</div>' +
'</article>';
}
document.addEventListener('DOMContentLoaded', function () {
var app = document.querySelector('.kgvvm-chat-app');
if (!app || typeof window.kgvvmChatConfig === 'undefined') {
return;
}
var config = window.kgvvmChatConfig;
var messagesEl = app.querySelector('[data-chat-messages]');
var form = app.querySelector('[data-chat-form]');
var input = app.querySelector('[data-chat-input]');
var status = app.querySelector('[data-chat-status]');
var roomButtons = app.querySelectorAll('[data-chat-room]');
var roomTitle = app.querySelector('[data-chat-room-title]');
var roomDescription = app.querySelector('[data-chat-room-description]');
var currentRoom = app.getAttribute('data-room') || 'general';
var initialRoom = currentRoom;
var currentUserId = Number(app.getAttribute('data-current-user-id') || '0');
var sendLocked = false;
var storageKey = 'kgvvmActiveChatRoom';
function getAvailableRooms() {
return Array.prototype.map.call(roomButtons, function (button) {
return button.getAttribute('data-chat-room') || '';
}).filter(Boolean);
}
function syncRoomState() {
try {
window.localStorage.setItem(storageKey, currentRoom);
} catch (error) {
// ignore storage errors
}
if (window.history && typeof window.history.replaceState === 'function') {
var url = new window.URL(window.location.href);
url.searchParams.set('room', currentRoom);
window.history.replaceState({}, '', url.toString());
}
}
function restoreRoomState() {
var availableRooms = getAvailableRooms();
var url = new window.URL(window.location.href);
var requestedRoom = url.searchParams.get('room') || '';
var storedRoom = '';
try {
storedRoom = window.localStorage.getItem(storageKey) || '';
} catch (error) {
storedRoom = '';
}
var preferredRoom = requestedRoom || storedRoom;
if (preferredRoom && availableRooms.indexOf(preferredRoom) !== -1) {
currentRoom = preferredRoom;
app.setAttribute('data-room', currentRoom);
}
}
function setStatus(text, isError) {
if (!status) {
return;
}
status.textContent = text || '';
status.classList.toggle('is-error', !!isError);
}
function scrollToBottom() {
if (messagesEl) {
messagesEl.scrollTop = messagesEl.scrollHeight;
}
}
function updateRoomButtons() {
roomButtons.forEach(function (button) {
button.classList.toggle('is-active', button.getAttribute('data-chat-room') === currentRoom);
});
}
function updateRoomMeta() {
roomButtons.forEach(function (button) {
if (button.getAttribute('data-chat-room') !== currentRoom) {
return;
}
if (roomTitle) {
roomTitle.textContent = button.getAttribute('data-chat-label') || '';
}
if (roomDescription) {
roomDescription.textContent = button.getAttribute('data-chat-description') || '';
}
});
}
function renderMessages(messages, replaceAll) {
if (!messagesEl) {
return;
}
if (replaceAll) {
messagesEl.innerHTML = '';
}
if (!Array.isArray(messages) || !messages.length) {
if (replaceAll) {
messagesEl.innerHTML = '<p class="kgvvm-chat-empty">' + escapeHtml(config.i18n.empty) + '</p>';
messagesEl.dataset.lastId = '0';
}
return;
}
if (messagesEl.querySelector('.kgvvm-chat-empty')) {
messagesEl.innerHTML = '';
}
messages.forEach(function (message) {
messagesEl.insertAdjacentHTML('beforeend', renderMessage(message, currentUserId));
messagesEl.dataset.lastId = String(message.id || messagesEl.dataset.lastId || '0');
});
scrollToBottom();
}
function fetchMessages(replaceAll) {
var data = new window.FormData();
data.append('action', 'kgvvm_fetch_chat_messages');
data.append('nonce', config.nonce);
data.append('room', currentRoom);
if (!replaceAll && messagesEl && messagesEl.dataset.lastId) {
data.append('after_id', messagesEl.dataset.lastId);
}
return window.fetch(config.ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
body: data
})
.then(function (response) { return response.json(); })
.then(function (payload) {
if (!payload || !payload.success) {
return;
}
renderMessages(payload.data.messages || [], replaceAll);
})
.catch(function () {
setStatus(config.i18n.fetchError, true);
});
}
roomButtons.forEach(function (button) {
button.addEventListener('click', function () {
currentRoom = button.getAttribute('data-chat-room') || 'general';
app.setAttribute('data-room', currentRoom);
syncRoomState();
updateRoomMeta();
if (messagesEl) {
messagesEl.dataset.lastId = '0';
}
updateRoomButtons();
setStatus(config.i18n.loading, false);
fetchMessages(true).then(function () {
setStatus('', false);
});
});
});
if (form && input) {
form.addEventListener('submit', function (event) {
event.preventDefault();
if (sendLocked) {
return;
}
var message = String(input.value || '').trim();
if (!message) {
return;
}
sendLocked = true;
setStatus(config.i18n.sending, false);
var data = new window.FormData();
data.append('action', 'kgvvm_send_chat_message');
data.append('nonce', config.nonce);
data.append('room', currentRoom);
data.append('message', message);
window.fetch(config.ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
body: data
})
.then(function (response) { return response.json(); })
.then(function (payload) {
sendLocked = false;
if (!payload || !payload.success) {
setStatus(config.i18n.sendError, true);
return;
}
renderMessages(payload.data.messages || [], false);
input.value = '';
input.focus();
setStatus('', false);
})
.catch(function () {
sendLocked = false;
setStatus(config.i18n.sendError, true);
});
});
}
restoreRoomState();
updateRoomButtons();
updateRoomMeta();
syncRoomState();
if (currentRoom !== initialRoom) {
fetchMessages(true);
}
scrollToBottom();
window.setInterval(function () {
fetchMessages(false);
}, Number(config.refreshInterval || 7000));
});
})();