Initial plugin commit

This commit is contained in:
2026-04-12 22:22:59 +02:00
commit 684477df4b
6 changed files with 642 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
Thumbs.db
*.zip

6
assets/css/admin.css Executable file
View File

@@ -0,0 +1,6 @@
.kgv-admin-wrap .kgv-admin-card{background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:20px;margin-top:16px}
.kgv-admin-header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.kgv-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.kgv-form-field{display:flex;flex-direction:column;gap:6px}.kgv-form-field-full{grid-column:1/-1}
.kgv-form-field input,.kgv-form-field select,.kgv-form-field textarea{width:100%}
.kgv-admin-actions{display:flex;gap:10px;margin-top:16px}
@media (max-width:782px){.kgv-form-grid{grid-template-columns:1fr}}

88
assets/css/front.css Executable file
View File

@@ -0,0 +1,88 @@
.kgv-termine-wrap{display:grid;gap:18px}
.kgv-termin-card,.kgv-termin-single{background:#fff;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.05)}
.kgv-termin-card-inner,.kgv-termin-single-inner{padding:20px}
.kgv-termin-datebox{display:inline-block;margin-bottom:12px;font-size:14px;font-weight:600;color:#6b7280}
.kgv-termin-title{margin:0 0 10px;font-size:1.3rem;line-height:1.2}
.kgv-termin-meta{display:flex;gap:14px;flex-wrap:wrap;color:#4b5563;font-size:14px;margin-bottom:12px}
.kgv-termin-excerpt,.kgv-termin-content{color:#374151;line-height:1.65}
.kgv-termin-actions{margin-top:16px}.kgv-termin-btn,.kgv-termin-back{display:inline-block;padding:10px 14px;border-radius:10px;text-decoration:none;background:#e5e7eb;color:#111827}
.kgv-termin-single-hero{padding:20px;background:#f3f4f6;border-bottom:1px solid #e5e7eb}.kgv-termin-single-hero h2{margin:0 0 10px}
.kgv-termin-back{margin-bottom:14px}
.kgv-termine-empty,.kgv-termine-notice{padding:16px;border:1px solid #e5e7eb;border-radius:12px;background:#fff}
.kgv-termin-single-hero {
position: relative;
}
.kgv-termin-single-hero .kgv-termin-datebox {
position: absolute;
top: 20px;
right: 20px;
font-size: 16px;
font-weight: 600;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 6px 10px;
color: #111827;
}
.kgv-termin-single-hero .kgv-termin-datebox {
position: absolute;
top: 20px;
right: 20px;
font-size: 16px;
font-weight: 600;
background: none;
border: none;
padding: 0;
color: #6b7280;
}
.kgv-sidebar-box{
padding:12px;
}
.kgv-sidebar-title{
margin:0 0 8px;
}
.kgv-sidebar-list{
display:grid;
gap:2px;
}
.kgv-sidebar-item{
display:grid;
grid-template-columns:80px 1fr;
align-items:center;
gap:8px;
text-decoration:none;
padding:3px 0;
border-radius:0;
}
.kgv-sidebar-item:hover{
background:none;
}
.kgv-sidebar-date{
font-size:12px;
font-weight:600;
color:#6b7280;
white-space:nowrap;
}
.kgv-sidebar-text{
font-size:13px;
color:#111827;
line-height:1.2;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}

View File

@@ -0,0 +1,520 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class KGV_Termine_Plugin {
private static $instance = null;
private $table_name;
public static function instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . KGV_TERMINE_TABLE;
register_activation_hook(KGV_TERMINE_FILE, array($this, 'activate'));
add_action('admin_menu', array($this, 'register_admin_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_front_assets'));
add_action('init', array($this, 'register_shortcodes'));
add_action('admin_post_kgv_save_termin', array($this, 'handle_save_termin'));
add_action('admin_post_kgv_delete_termin', array($this, 'handle_delete_termin'));
add_filter('query_vars', array($this, 'register_query_vars'));
}
public function activate() {
$this->create_table();
}
private function create_table() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$this->table_name} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
event_date DATETIME NOT NULL,
owner VARCHAR(255) NULL,
location VARCHAR(255) NULL,
summary TEXT NULL,
description LONGTEXT NULL,
is_published TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY event_date (event_date),
KEY is_published (is_published)
) {$charset_collate};";
dbDelta($sql);
}
public function enqueue_admin_assets($hook) {
if (strpos((string) $hook, 'kgv-termine') === false) {
return;
}
wp_enqueue_style(
'kgv-termine-admin-style',
KGV_TERMINE_URL . 'assets/css/admin.css',
array(),
'1.0.2'
);
}
public function enqueue_front_assets() {
wp_enqueue_style(
'kgv-termine-front-style',
KGV_TERMINE_URL . 'assets/css/front.css',
array(),
'1.0.2'
);
}
public function register_query_vars($vars) {
$vars[] = 'kgv_termin';
return $vars;
}
public function register_admin_menu() {
add_menu_page(
'Termine',
'Termine',
'manage_options',
'kgv-termine',
array($this, 'render_list_page'),
'dashicons-calendar-alt',
25
);
add_submenu_page(
'kgv-termine',
'Alle Termine',
'Alle Termine',
'manage_options',
'kgv-termine',
array($this, 'render_list_page')
);
add_submenu_page(
'kgv-termine',
'Neuer Termin',
'Neuer Termin',
'manage_options',
'kgv-termine-new',
array($this, 'render_form_page')
);
}
public function register_shortcodes() {
add_shortcode('kgv_termine', array($this, 'render_termine_shortcode'));
add_shortcode('kgv_termin_detail', array($this, 'render_detail_shortcode'));
add_shortcode('kgv_termine_sidebar', array($this, 'render_sidebar_shortcode'));
}
public function handle_save_termin() {
if (!current_user_can('manage_options')) {
wp_die('Keine Berechtigung.');
}
check_admin_referer('kgv_save_termin');
$this->save_termin_from_request();
}
public function handle_delete_termin() {
if (!current_user_can('manage_options')) {
wp_die('Keine Berechtigung.');
}
$id = isset($_GET['termin_id']) ? absint($_GET['termin_id']) : 0;
if (!$id || !isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'kgv_delete_termin_' . $id)) {
wp_safe_redirect(admin_url('admin.php?page=kgv-termine&message=delete_failed'));
exit;
}
global $wpdb;
$wpdb->delete($this->table_name, array('id' => $id), array('%d'));
wp_safe_redirect(admin_url('admin.php?page=kgv-termine&message=deleted'));
exit;
}
private function save_termin_from_request() {
global $wpdb;
$id = isset($_POST['termin_id']) ? absint($_POST['termin_id']) : 0;
$title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
$event_date = sanitize_text_field(wp_unslash($_POST['event_date'] ?? ''));
$owner = sanitize_text_field(wp_unslash($_POST['owner'] ?? ''));
$location = sanitize_text_field(wp_unslash($_POST['location'] ?? ''));
$summary = sanitize_textarea_field(wp_unslash($_POST['summary'] ?? ''));
$description = wp_kses_post(wp_unslash($_POST['description'] ?? ''));
$is_published = isset($_POST['is_published']) ? (int) wp_unslash($_POST['is_published']) : 1;
$is_published = $is_published === 1 ? 1 : 0;
if ($title === '' || $event_date === '' || strtotime($event_date) === false) {
$target = $id ? admin_url('admin.php?page=kgv-termine-new&termin_id=' . $id . '&message=missing') : admin_url('admin.php?page=kgv-termine-new&message=missing');
wp_safe_redirect($target);
exit;
}
$mysql_date = date('Y-m-d H:i:s', strtotime($event_date));
$now = current_time('mysql');
$data = array(
'title' => $title,
'event_date' => $mysql_date,
'owner' => $owner,
'location' => $location,
'summary' => $summary,
'description' => $description,
'is_published' => $is_published,
'updated_at' => $now,
);
$formats = array('%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s');
if ($id) {
$wpdb->update($this->table_name, $data, array('id' => $id), $formats, array('%d'));
wp_safe_redirect(admin_url('admin.php?page=kgv-termine&message=updated'));
exit;
}
$data['created_at'] = $now;
$wpdb->insert($this->table_name, $data, array('%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s'));
wp_safe_redirect(admin_url('admin.php?page=kgv-termine&message=created'));
exit;
}
public function render_list_page() {
global $wpdb;
$rows = $wpdb->get_results("SELECT * FROM {$this->table_name} ORDER BY event_date ASC, id ASC");
$message = isset($_GET['message']) ? sanitize_text_field(wp_unslash($_GET['message'])) : '';
?>
<div class="wrap kgv-admin-wrap">
<div class="kgv-admin-header">
<h1>Termine</h1>
<a href="<?php echo esc_url(admin_url('admin.php?page=kgv-termine-new')); ?>" class="page-title-action">Neuen Termin anlegen</a>
</div>
<?php $this->render_notice($message); ?>
<div class="kgv-admin-card">
<table class="widefat striped kgv-admin-table">
<thead>
<tr>
<th>Titel</th>
<th>Datum</th>
<th>Verantwortlich</th>
<th>Ort</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)) : ?>
<tr>
<td colspan="6">Noch keine Termine angelegt.</td>
</tr>
<?php else : ?>
<?php foreach ($rows as $row) : ?>
<tr>
<td><strong><?php echo esc_html($row->title); ?></strong></td>
<td><?php echo esc_html(wp_date('d.m.Y H:i', strtotime($row->event_date))); ?></td>
<td><?php echo esc_html($row->owner ?: '—'); ?></td>
<td><?php echo esc_html($row->location ?: '—'); ?></td>
<td><?php echo (int) $row->is_published === 1 ? 'Veröffentlicht' : 'Entwurf'; ?></td>
<td>
<a href="<?php echo esc_url(admin_url('admin.php?page=kgv-termine-new&termin_id=' . (int) $row->id)); ?>">Bearbeiten</a>
|
<a href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=kgv_delete_termin&termin_id=' . (int) $row->id), 'kgv_delete_termin_' . (int) $row->id)); ?>" onclick="return confirm('Termin wirklich löschen?');">Löschen</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php
}
public function render_form_page() {
global $wpdb;
$id = isset($_GET['termin_id']) ? absint($_GET['termin_id']) : 0;
$message = isset($_GET['message']) ? sanitize_text_field(wp_unslash($_GET['message'])) : '';
$row = null;
if ($id) {
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id));
}
$title = $row ? $row->title : '';
$event_date = $row ? date('Y-m-d\TH:i', strtotime($row->event_date)) : '';
$owner = $row ? $row->owner : '';
$location = $row ? $row->location : '';
$summary = $row ? $row->summary : '';
$description = $row ? $row->description : '';
$is_published = $row ? (int) $row->is_published : 1;
?>
<div class="wrap kgv-admin-wrap">
<div class="kgv-admin-header">
<h1><?php echo $id ? 'Termin bearbeiten' : 'Neuen Termin anlegen'; ?></h1>
</div>
<?php $this->render_notice($message); ?>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="kgv-admin-form">
<?php wp_nonce_field('kgv_save_termin'); ?>
<input type="hidden" name="action" value="kgv_save_termin">
<input type="hidden" name="termin_id" value="<?php echo esc_attr($id); ?>">
<div class="kgv-admin-card">
<div class="kgv-form-grid">
<div class="kgv-form-field kgv-form-field-full">
<label for="kgv-title">Titel</label>
<input type="text" id="kgv-title" name="title" value="<?php echo esc_attr($title); ?>" required>
</div>
<div class="kgv-form-field">
<label for="kgv-event-date">Datum / Uhrzeit</label>
<input type="datetime-local" id="kgv-event-date" name="event_date" value="<?php echo esc_attr($event_date); ?>" required>
</div>
<div class="kgv-form-field">
<label for="kgv-owner">Verantwortlich</label>
<input type="text" id="kgv-owner" name="owner" value="<?php echo esc_attr($owner); ?>">
</div>
<div class="kgv-form-field">
<label for="kgv-location">Ort</label>
<input type="text" id="kgv-location" name="location" value="<?php echo esc_attr($location); ?>">
</div>
<div class="kgv-form-field">
<label for="kgv-published">Status</label>
<select id="kgv-published" name="is_published">
<option value="1" <?php selected($is_published, 1); ?>>Veröffentlicht</option>
<option value="0" <?php selected($is_published, 0); ?>>Entwurf</option>
</select>
</div>
<div class="kgv-form-field kgv-form-field-full">
<label for="kgv-summary">Kurzbeschreibung</label>
<textarea id="kgv-summary" name="summary" rows="3"><?php echo esc_textarea($summary); ?></textarea>
</div>
<div class="kgv-form-field kgv-form-field-full">
<label for="kgv-description">Detailbeschreibung</label>
<textarea id="kgv-description" name="description" rows="8"><?php echo esc_textarea($description); ?></textarea>
</div>
</div>
</div>
<div class="kgv-admin-actions">
<button type="submit" class="button button-primary">Termin speichern</button>
<a href="<?php echo esc_url(admin_url('admin.php?page=kgv-termine')); ?>" class="button">Abbrechen</a>
</div>
</form>
</div>
<?php
}
private function render_notice($message) {
$map = array(
'created' => array('success', 'Termin wurde angelegt.'),
'updated' => array('success', 'Termin wurde aktualisiert.'),
'deleted' => array('success', 'Termin wurde gelöscht.'),
'delete_failed' => array('error', 'Termin konnte nicht gelöscht werden.'),
'missing' => array('error', 'Titel und Datum sind Pflichtfelder.'),
);
if (!isset($map[$message])) {
return;
}
list($type, $text) = $map[$message];
printf('<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>', esc_attr($type), esc_html($text));
}
private function get_frontend_rows($limit = 0) {
global $wpdb;
if ($limit > 0) {
return $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->table_name} WHERE is_published = 1 ORDER BY event_date ASC, id ASC LIMIT %d",
$limit
));
}
return $wpdb->get_results("SELECT * FROM {$this->table_name} WHERE is_published = 1 ORDER BY event_date ASC, id ASC");
}
private function get_row($id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id));
}
public function render_termine_shortcode($atts) {
$atts = shortcode_atts(array(
'limit' => 0,
'detail' => 1,
), $atts, 'kgv_termine');
$detail_id = absint(get_query_var('kgv_termin'));
if (!$detail_id && isset($_GET['kgv_termin'])) {
$detail_id = absint(wp_unslash($_GET['kgv_termin']));
}
$base_url = $this->get_current_page_url(false);
if ($detail_id && (int) $atts['detail'] === 1) {
return $this->render_detail_by_id($detail_id, $base_url);
}
$rows = $this->get_frontend_rows(absint($atts['limit']));
if (empty($rows)) {
return '<div class="kgv-termine-empty">Aktuell sind keine Termine verfügbar.</div>';
}
ob_start();
echo '<div class="kgv-termine-wrap">';
foreach ($rows as $row) {
$link = add_query_arg('kgv_termin', (int) $row->id, $base_url);
echo '<article class="kgv-termin-card">';
echo '<div class="kgv-termin-card-inner">';
echo '<div class="kgv-termin-datebox">' . esc_html(wp_date('d.m.Y H:i', strtotime($row->event_date))) . '</div>';
echo '<h3 class="kgv-termin-title">' . esc_html($row->title) . '</h3>';
echo '<div class="kgv-termin-meta">';
if (!empty($row->owner)) {
echo '<span>Verantwortlich: ' . esc_html($row->owner) . '</span>';
}
if (!empty($row->location)) {
echo '<span>Ort: ' . esc_html($row->location) . '</span>';
}
echo '</div>';
if (!empty($row->summary)) {
echo '<div class="kgv-termin-excerpt">' . esc_html($row->summary) . '</div>';
}
echo '<div class="kgv-termin-actions"><a class="kgv-termin-btn" href="' . esc_url($link) . '">Details ansehen</a></div>';
echo '</div>';
echo '</article>';
}
echo '</div>';
return ob_get_clean();
}
public function render_detail_shortcode($atts) {
$atts = shortcode_atts(array(
'id' => 0,
), $atts, 'kgv_termin_detail');
$id = absint($atts['id']);
if (!$id) {
return '<div class="kgv-termine-notice">Keine Termin-ID übergeben.</div>';
}
return $this->render_detail_by_id($id, '');
}
private function render_detail_by_id($id, $back_url = '') {
$row = $this->get_row($id);
if (!$row || !(int) $row->is_published) {
return '<div class="kgv-termine-notice">Termin nicht gefunden.</div>';
}
ob_start();
echo '<article class="kgv-termin-single">';
echo '<div class="kgv-termin-single-hero">';
if ($back_url) {
echo '<a class="kgv-termin-back" href="' . esc_url($back_url) . '">← Zurück zur Übersicht</a>';
}
echo '<div class="kgv-termin-datebox">' . esc_html(wp_date('d.m.Y H:i', strtotime($row->event_date))) . '</div>';
echo '<h2>' . esc_html($row->title) . '</h2>';
echo '<div class="kgv-termin-meta">';
if (!empty($row->owner)) {
echo '<span>Verantwortlich: ' . esc_html($row->owner) . '</span>';
}
if (!empty($row->location)) {
echo '<span>Ort: ' . esc_html($row->location) . '</span>';
}
echo '</div>';
echo '</div>';
echo '<div class="kgv-termin-single-inner">';
if (!empty($row->summary)) {
echo '<p class="kgv-termin-summary"><strong>' . esc_html($row->summary) . '</strong></p>';
}
echo '<div class="kgv-termin-content">' . wpautop(wp_kses_post($row->description)) . '</div>';
echo '</div>';
echo '</article>';
return ob_get_clean();
}
public function render_sidebar_shortcode($atts) {
$atts = shortcode_atts(array(
'limit' => 5,
), $atts, 'kgv_termine_sidebar');
$rows = $this->get_frontend_rows(max(1, min(20, absint($atts['limit']))));
if (empty($rows)) {
return '<div class="kgv-sidebar-box">Keine Termine</div>';
}
$base_url = $this->get_current_page_url(false);
ob_start();
echo '<div class="kgv-sidebar-box">';
echo '<h3 class="kgv-sidebar-title">Nächste Termine</h3>';
echo '<div class="kgv-sidebar-list">';
foreach ($rows as $row) {
$link = add_query_arg('kgv_termin', (int) $row->id, $base_url);
echo '<a class="kgv-sidebar-item" href="' . esc_url($link) . '">';
echo '<span class="kgv-sidebar-date">' . esc_html(wp_date('d.m.Y', strtotime($row->event_date))) . '</span>';
echo '<span class="kgv-sidebar-text">' . esc_html($row->title) . '</span>';
echo '</a>';
}
echo '</div>';
echo '</div>';
return ob_get_clean();
}
private function get_current_page_url($with_query = true) {
$page_url = '';
if (is_singular() && get_queried_object_id()) {
$page_url = get_permalink(get_queried_object_id());
}
if (!$page_url) {
global $wp;
$page_url = home_url(add_query_arg(array(), $wp->request ? '/' . ltrim($wp->request, '/') . '/' : '/'));
}
if (!$with_query) {
return remove_query_arg(array_keys($_GET), $page_url);
}
return $page_url;
}
}

24
kgv-termine-plugin.php Executable file
View File

@@ -0,0 +1,24 @@
<?php
/**
* Plugin Name: KGV Termine
* Plugin URI: https://apex-project.de/
* Description: Eigene Terminverwaltung mit separatem Admin-Interface, Frontend-Liste und Detailansicht per Shortcode.
* Update URI: https://git.apex-project.de/RonnyG/KGV-Termine
* GitHub Plugin URI: https://git.apex-project.de/RonnyG/KGV-Termine
* Version: 1.0.2
* Author: Ronny Grobel
* Text Domain: kgv-termine-admin
*/
if (!defined('ABSPATH')) {
exit;
}
define('KGV_TERMINE_FILE', __FILE__);
define('KGV_TERMINE_PATH', plugin_dir_path(__FILE__));
define('KGV_TERMINE_URL', plugin_dir_url(__FILE__));
define('KGV_TERMINE_TABLE', 'kgv_termine');
require_once KGV_TERMINE_PATH . 'includes/class-kgv-termine-plugin.php';
KGV_Termine_Plugin::instance();

1
readme.txt Executable file
View File

@@ -0,0 +1 @@
KGV Termine Admin 1.0.2