feat: Implement posts management functionality in the admin panel.

This commit is contained in:
2026-02-26 23:28:21 +01:00
parent 7db066a23b
commit 07a33520fc
9 changed files with 1450 additions and 25 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
admin/.DS_Store vendored

Binary file not shown.

View File

@@ -18,7 +18,7 @@
--igsp-gray: #6c757d;
--igsp-light: #f8f9fa;
--igsp-border: #e2e4e7;
--igsp-shadow: 0 2px 8px rgba(0,0,0,0.08);
--igsp-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--igsp-radius: 8px;
}
@@ -414,8 +414,15 @@
}
@keyframes igsp-progress-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.igsp-progress-text {
@@ -515,8 +522,13 @@
top: 0;
}
.igsp-log-type { width: 100px; }
.igsp-log-date { width: 120px; }
.igsp-log-type {
width: 100px;
}
.igsp-log-date {
width: 120px;
}
.igsp-log-badge {
display: inline-block;
@@ -527,10 +539,25 @@
text-transform: uppercase;
}
.igsp-log-success { background: rgba(40, 167, 69, 0.1); color: var(--igsp-success); }
.igsp-log-error { background: rgba(220, 53, 69, 0.1); color: var(--igsp-error); }
.igsp-log-warning { background: rgba(255, 193, 7, 0.1); color: #856404; }
.igsp-log-info { background: rgba(23, 162, 184, 0.1); color: var(--igsp-info); }
.igsp-log-success {
background: rgba(40, 167, 69, 0.1);
color: var(--igsp-success);
}
.igsp-log-error {
background: rgba(220, 53, 69, 0.1);
color: var(--igsp-error);
}
.igsp-log-warning {
background: rgba(255, 193, 7, 0.1);
color: #856404;
}
.igsp-log-info {
background: rgba(23, 162, 184, 0.1);
color: var(--igsp-info);
}
.igsp-no-logs {
padding: 40px;
@@ -614,12 +641,12 @@
.igsp-admin-wrap {
grid-template-columns: 1fr;
}
.igsp-admin-sidebar {
flex-direction: row;
flex-wrap: wrap;
}
.igsp-sidebar-box {
flex: 1;
min-width: 250px;
@@ -631,19 +658,456 @@
flex-wrap: wrap;
padding: 10px;
}
.igsp-tab-link {
flex: 1;
justify-content: center;
padding: 10px;
font-size: 12px;
}
.igsp-tab-link .dashicons {
display: none;
}
.igsp-layout-selector {
grid-template-columns: repeat(2, 1fr);
}
}
/* ============================================
Posts Management Tab
============================================ */
/* Posts Header */
.igsp-posts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.igsp-posts-header h2 {
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
.igsp-post-count {
font-weight: 400;
color: var(--igsp-gray);
font-size: 14px;
}
/* Search */
.igsp-search-wrapper {
display: flex;
gap: 5px;
}
.igsp-search-input {
min-width: 220px;
padding: 6px 12px;
border: 1px solid var(--igsp-border);
border-radius: var(--igsp-radius);
}
/* Posts Table */
.igsp-posts-table-wrapper {
border: 1px solid var(--igsp-border);
border-radius: var(--igsp-radius);
overflow: hidden;
}
.igsp-posts-table {
margin: 0 !important;
border: none !important;
}
.igsp-posts-table th {
background: var(--igsp-light);
font-weight: 600;
font-size: 13px;
padding: 12px 15px;
}
.igsp-posts-table td {
padding: 10px 15px;
vertical-align: middle;
}
.igsp-col-thumb {
width: 70px;
}
.igsp-col-type {
width: 80px;
}
.igsp-col-status {
width: 80px;
}
.igsp-col-link {
width: 50px;
text-align: center;
}
.igsp-col-date {
width: 150px;
}
.igsp-col-actions {
width: 100px;
}
/* Post Thumbnail */
.igsp-post-thumb {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 6px;
display: block;
}
.igsp-post-thumb-placeholder {
width: 50px;
height: 50px;
background: var(--igsp-light);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: var(--igsp-gray);
}
.igsp-post-thumb-placeholder .dashicons {
font-size: 24px;
width: 24px;
height: 24px;
}
/* Caption */
.igsp-caption-text {
font-size: 13px;
color: var(--igsp-dark);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Post Link */
.igsp-post-link {
color: var(--igsp-primary);
text-decoration: none;
}
.igsp-post-link:hover {
color: var(--igsp-primary-hover);
}
.igsp-post-link .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
}
.igsp-no-link {
color: var(--igsp-gray);
}
/* Type Badges */
.igsp-type-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.igsp-type-synced {
background: rgba(23, 162, 184, 0.1);
color: var(--igsp-info);
}
.igsp-type-manual {
background: rgba(225, 48, 108, 0.1);
color: var(--igsp-primary);
}
/* Status Badges */
.igsp-status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.igsp-status-active {
background: rgba(40, 167, 69, 0.1);
color: var(--igsp-success);
}
.igsp-status-inactive {
background: rgba(220, 53, 69, 0.1);
color: var(--igsp-error);
}
/* Inactive Row */
.igsp-post-inactive {
opacity: 0.6;
}
.igsp-post-inactive:hover {
opacity: 1;
}
/* Action Buttons */
.igsp-action-buttons {
display: flex;
gap: 5px;
}
.igsp-action-buttons .button {
padding: 2px 6px;
min-height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.igsp-action-buttons .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
.igsp-delete-post {
color: var(--igsp-error) !important;
border-color: var(--igsp-error) !important;
}
.igsp-delete-post:hover {
background: var(--igsp-error) !important;
color: #fff !important;
}
/* Post Date */
.igsp-post-date {
font-size: 12px;
color: var(--igsp-gray);
}
/* No Posts */
.igsp-no-posts {
padding: 40px;
text-align: center;
color: var(--igsp-gray);
background: var(--igsp-light);
border-radius: var(--igsp-radius);
}
/* ============================================
Pagination
============================================ */
.igsp-pagination {
display: flex;
align-items: center;
gap: 5px;
margin-top: 20px;
flex-wrap: wrap;
}
.igsp-pagination .button {
min-width: 36px;
text-align: center;
}
.igsp-pagination-info {
margin-left: 15px;
font-size: 13px;
color: var(--igsp-gray);
}
/* ============================================
Manual Post Form
============================================ */
.igsp-manual-post-form {
background: var(--igsp-light);
border-radius: var(--igsp-radius);
padding: 25px;
}
.igsp-manual-post-fields {
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: start;
}
.igsp-manual-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.igsp-manual-field label {
font-weight: 600;
font-size: 13px;
color: var(--igsp-dark);
}
.igsp-manual-field .required {
color: var(--igsp-error);
}
.igsp-manual-image-field {
grid-row: span 4;
}
/* Image Upload Area */
.igsp-image-upload-area {
width: 200px;
height: 200px;
border: 2px dashed var(--igsp-border);
border-radius: var(--igsp-radius);
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.2s;
background: #fff;
}
.igsp-image-upload-area:hover {
border-color: var(--igsp-primary);
}
.igsp-upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 8px;
color: var(--igsp-gray);
}
.igsp-upload-placeholder .dashicons {
font-size: 40px;
width: 40px;
height: 40px;
color: var(--igsp-border);
}
.igsp-upload-placeholder span:last-child {
font-size: 13px;
font-weight: 500;
}
/* Select Image Button */
#igsp-select-image-btn {
margin-top: 10px;
display: inline-flex;
align-items: center;
gap: 6px;
width: 200px;
justify-content: center;
}
#igsp-select-image-btn .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
/* Image Preview */
.igsp-image-preview {
width: 100%;
height: 100%;
position: relative;
}
.igsp-image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.igsp-remove-image {
position: absolute;
top: 5px;
right: 5px;
background: rgba(220, 53, 69, 0.9);
color: #fff;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
transition: background 0.2s;
}
.igsp-remove-image:hover {
background: var(--igsp-error);
}
.igsp-remove-image .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
/* Manual Post Actions */
.igsp-manual-post-actions {
margin-top: 20px;
display: flex;
align-items: center;
gap: 15px;
}
.igsp-manual-post-actions .button {
display: flex;
align-items: center;
gap: 6px;
}
.igsp-manual-post-actions .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
@media (max-width: 782px) {
.igsp-manual-post-fields {
grid-template-columns: 1fr;
}
.igsp-manual-image-field {
grid-row: auto;
}
.igsp-image-upload-area {
width: 100%;
height: 150px;
}
.igsp-posts-header {
flex-direction: column;
align-items: flex-start;
}
.igsp-col-date,
.igsp-col-type {
display: none;
}
}

View File

@@ -25,18 +25,27 @@
this.initSyncButton();
this.initToolButtons();
this.initTabPersistence();
this.initPostsTab();
this.initManualPostForm();
},
/**
* Initialize color pickers
*/
initColorPickers: function () {
$('.igsp-color-picker').wpColorPicker({
change: function (event, ui) {
// Update live preview
IGSPAdmin.updatePreview();
}
});
if ($('.igsp-color-picker').length === 0) {
return;
}
try {
$('.igsp-color-picker').wpColorPicker({
change: function (event, ui) {
// Update live preview
IGSPAdmin.updatePreview();
}
});
} catch (e) {
// Color picker not available on this tab
}
},
/**
@@ -233,6 +242,210 @@
});
},
/**
* Initialize Posts Tab functionality
*/
initPostsTab: function () {
// Toggle post active/inactive
$(document).on('click', '.igsp-toggle-post', function () {
const $button = $(this);
const postId = $button.data('post-id');
const $row = $button.closest('.igsp-post-row');
$button.prop('disabled', true);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_toggle_post',
nonce: igspAdmin.nonce,
post_id: postId
}, function (response) {
if (response.success) {
const isActive = response.data.is_active;
const $icon = $button.find('.dashicons');
const $badge = $row.find('.igsp-status-badge');
if (isActive) {
$row.removeClass('igsp-post-inactive');
$icon.removeClass('dashicons-visibility').addClass('dashicons-hidden');
$badge.removeClass('igsp-status-inactive').addClass('igsp-status-active').text('Aktiv');
$button.attr('title', 'Deaktivieren');
} else {
$row.addClass('igsp-post-inactive');
$icon.removeClass('dashicons-hidden').addClass('dashicons-visibility');
$badge.removeClass('igsp-status-active').addClass('igsp-status-inactive').text('Inaktiv');
$button.attr('title', 'Aktivieren');
}
}
$button.prop('disabled', false);
}).fail(function () {
$button.prop('disabled', false);
});
});
// Delete post
$(document).on('click', '.igsp-delete-post', function () {
if (!confirm(igspAdmin.strings.confirmDeletePost)) {
return;
}
const $button = $(this);
const postId = $button.data('post-id');
const $row = $button.closest('.igsp-post-row');
$button.prop('disabled', true);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_delete_post',
nonce: igspAdmin.nonce,
post_id: postId
}, function (response) {
if (response.success) {
$row.fadeOut(300, function () {
$(this).remove();
// Update count
const $count = $('.igsp-post-count');
const currentCount = parseInt($count.text().replace(/\D/g, '')) || 0;
$count.text('(' + Math.max(0, currentCount - 1) + ')');
});
} else {
$button.prop('disabled', false);
}
}).fail(function () {
$button.prop('disabled', false);
});
});
},
/**
* Initialize Manual Post Form
*/
initManualPostForm: function () {
// Only init if the button exists on this page
if ($('#igsp-select-image-btn').length === 0) {
return;
}
let mediaFrame = null;
let selectedAttachmentId = 0;
// Open media library via button
$('#igsp-select-image-btn').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
// Check if wp.media is available
if (typeof wp === 'undefined' || typeof wp.media === 'undefined') {
alert('WordPress Media Library ist nicht verfügbar. Bitte lade die Seite neu.');
return;
}
// If media frame already exists, reopen it
if (mediaFrame) {
mediaFrame.open();
return;
}
// Create media frame
mediaFrame = wp.media({
title: igspAdmin.strings.selectImage,
button: {
text: igspAdmin.strings.useImage
},
multiple: false,
library: {
type: 'image'
}
});
// When image is selected
mediaFrame.on('select', function () {
const attachment = mediaFrame.state().get('selection').first().toJSON();
selectedAttachmentId = attachment.id;
$('#igsp-attachment-id').val(attachment.id);
// Show preview
const previewUrl = attachment.sizes && attachment.sizes.medium
? attachment.sizes.medium.url
: attachment.url;
$('#igsp-preview-img').attr('src', previewUrl);
$('#igsp-image-preview').show();
$('#igsp-upload-placeholder').hide();
});
mediaFrame.open();
});
// Remove image
$('#igsp-remove-image').on('click', function (e) {
e.stopPropagation();
selectedAttachmentId = 0;
$('#igsp-attachment-id').val('');
$('#igsp-preview-img').attr('src', '');
$('#igsp-image-preview').hide();
$('#igsp-upload-placeholder').show();
});
// Submit manual post
$('#igsp-add-manual-post').on('click', function () {
const $button = $(this);
const $result = $('#igsp-manual-post-result');
const attachmentId = $('#igsp-attachment-id').val();
if (!attachmentId) {
$result.removeClass('success').addClass('error').text('Bitte wähle ein Bild aus.').show();
return;
}
$button.prop('disabled', true);
$result.hide();
// Convert datetime-local to MySQL format
let postedAt = $('#igsp-manual-date').val();
if (postedAt) {
postedAt = postedAt.replace('T', ' ') + ':00';
}
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_add_manual_post',
nonce: igspAdmin.nonce,
attachment_id: attachmentId,
post_url: $('#igsp-manual-url').val(),
caption: $('#igsp-manual-caption').val(),
posted_at: postedAt
}, function (response) {
$result.removeClass('success error');
if (response.success) {
$result.addClass('success').text(response.data.message).show();
// Reset form
selectedAttachmentId = 0;
$('#igsp-attachment-id').val('');
$('#igsp-preview-img').attr('src', '');
$('#igsp-image-preview').hide();
$('#igsp-upload-placeholder').show();
$('#igsp-manual-url').val('');
$('#igsp-manual-caption').val('');
$('#igsp-manual-date').val('');
// Reload after short delay to show new post in table
setTimeout(function () {
location.reload();
}, 1500);
} else {
$result.addClass('error').text(response.data.message).show();
}
$button.prop('disabled', false);
}).fail(function () {
$result.addClass('error').text('Ein Fehler ist aufgetreten.').show();
$button.prop('disabled', false);
});
});
},
/**
* Update sync status
*/

438
admin/views/tab-posts.php Normal file
View File

@@ -0,0 +1,438 @@
<?php
/**
* Posts Management Tab
*
* @package Instagram_Gallery_Sync_Pro
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$database = new IGSP_Database();
$image_handler = new IGSP_Image_Handler();
// Pagination
$per_page = 20;
$current_page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
$search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
$offset = ($current_page - 1) * $per_page;
// Get posts
$total_posts = $database->count_posts_filtered(array(
'active' => null,
'search' => $search,
));
$posts = $database->get_posts(array(
'limit' => $per_page,
'offset' => $offset,
'order' => 'newest',
'active' => false, // Show all posts (active + inactive)
'search' => $search,
));
$total_pages = ceil($total_posts / $per_page);
?>
<!-- Manual Post Form -->
<div class="igsp-section">
<h2>
<?php esc_html_e('Post manuell hinzufügen', 'instagram-gallery-sync-pro'); ?>
</h2>
<p class="description">
<?php esc_html_e('Füge manuell ein Bild/Post hinzu, z.B. wenn Instagram ein Rate-Limit verhängt hat.', 'instagram-gallery-sync-pro'); ?>
</p>
<div class="igsp-manual-post-form" id="igsp-manual-post-form">
<div class="igsp-manual-post-fields">
<div class="igsp-manual-field igsp-manual-image-field">
<label>
<?php esc_html_e('Bild', 'instagram-gallery-sync-pro'); ?> <span class="required">*</span>
</label>
<div class="igsp-image-upload-area" id="igsp-image-upload-area">
<div class="igsp-image-preview" id="igsp-image-preview" style="display: none;">
<img src="" alt="" id="igsp-preview-img">
<button type="button" class="igsp-remove-image" id="igsp-remove-image"
title="<?php esc_attr_e('Bild entfernen', 'instagram-gallery-sync-pro'); ?>">
<span class="dashicons dashicons-no-alt"></span>
</button>
</div>
<div class="igsp-upload-placeholder" id="igsp-upload-placeholder">
<span class="dashicons dashicons-format-image"></span>
<span><?php esc_html_e('Klicke auf den Button unten', 'instagram-gallery-sync-pro'); ?></span>
</div>
<input type="hidden" id="igsp-attachment-id" value="">
</div>
<button type="button" class="button" id="igsp-select-image-btn">
<span class="dashicons dashicons-format-image"></span>
<?php esc_html_e('Bild auswählen', 'instagram-gallery-sync-pro'); ?>
</button>
</div>
<div class="igsp-manual-field">
<label for="igsp-manual-url">
<?php esc_html_e('Instagram URL', 'instagram-gallery-sync-pro'); ?>
</label>
<input type="url" id="igsp-manual-url" placeholder="https://www.instagram.com/p/..."
class="regular-text">
<p class="description">
<?php esc_html_e('Link zum Instagram-Post (optional).', 'instagram-gallery-sync-pro'); ?>
</p>
</div>
<div class="igsp-manual-field">
<label for="igsp-manual-caption">
<?php esc_html_e('Caption', 'instagram-gallery-sync-pro'); ?>
</label>
<textarea id="igsp-manual-caption" rows="3" class="large-text"
placeholder="<?php esc_attr_e('Beschreibung des Posts...', 'instagram-gallery-sync-pro'); ?>"></textarea>
</div>
<div class="igsp-manual-field">
<label for="igsp-manual-date">
<?php esc_html_e('Datum', 'instagram-gallery-sync-pro'); ?>
</label>
<input type="datetime-local" id="igsp-manual-date" class="regular-text">
<p class="description">
<?php esc_html_e('Veröffentlichungsdatum (optional, Standard: jetzt).', 'instagram-gallery-sync-pro'); ?>
</p>
</div>
</div>
<div class="igsp-manual-post-actions">
<button type="button" id="igsp-add-manual-post" class="button button-primary">
<span class="dashicons dashicons-plus-alt2"></span>
<?php esc_html_e('Post hinzufügen', 'instagram-gallery-sync-pro'); ?>
</button>
<div id="igsp-manual-post-result" class="igsp-sync-result" style="display: none;"></div>
</div>
</div>
</div>
<!-- Posts Table -->
<div class="igsp-section">
<div class="igsp-posts-header">
<h2>
<?php esc_html_e('Alle Posts', 'instagram-gallery-sync-pro'); ?>
<span class="igsp-post-count">(
<?php echo esc_html($total_posts); ?>)
</span>
</h2>
<form method="get" class="igsp-search-form">
<input type="hidden" name="page" value="<?php echo esc_attr(IGSP_Admin::PAGE_SLUG); ?>">
<input type="hidden" name="tab" value="posts">
<div class="igsp-search-wrapper">
<input type="search" name="s" value="<?php echo esc_attr($search); ?>"
placeholder="<?php esc_attr_e('Posts durchsuchen...', 'instagram-gallery-sync-pro'); ?>"
class="igsp-search-input">
<button type="submit" class="button">
<span class="dashicons dashicons-search"></span>
</button>
</div>
</form>
</div>
<?php if (empty($posts)): ?>
<div class="igsp-no-posts">
<?php if (!empty($search)): ?>
<p>
<?php printf(esc_html__('Keine Posts gefunden für "%s".', 'instagram-gallery-sync-pro'), esc_html($search)); ?>
</p>
<?php else: ?>
<p>
<?php esc_html_e('Noch keine Posts vorhanden. Synchronisiere dein Instagram-Konto oder füge manuell Posts hinzu.', 'instagram-gallery-sync-pro'); ?>
</p>
<?php endif; ?>
</div>
<?php else: ?>
<div class="igsp-posts-table-wrapper">
<table class="igsp-posts-table widefat striped">
<thead>
<tr>
<th class="igsp-col-thumb">
<?php esc_html_e('Bild', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-col-caption">
<?php esc_html_e('Caption', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-col-link">
<?php esc_html_e('Link', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-col-type">
<?php esc_html_e('Typ', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-col-status">
<?php esc_html_e('Status', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-col-date">
<?php esc_html_e('Datum', 'instagram-gallery-sync-pro'); ?>
</th>
<th class="igsp-col-actions">
<?php esc_html_e('Aktionen', 'instagram-gallery-sync-pro'); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($posts as $post):
// Get image URL
$thumb_url = '';
if (!empty($post->image_thumbnail_path)) {
$thumb_url = $image_handler->get_image_url($post->image_thumbnail_path);
} elseif (!empty($post->image_local_path)) {
$thumb_url = $image_handler->get_image_url($post->image_local_path);
}
$is_manual = !empty($post->is_manual);
$is_active = !empty($post->is_active);
$caption_short = !empty($post->caption) ? wp_trim_words($post->caption, 12, '...') : '—';
$posted_date = !empty($post->posted_at) ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($post->posted_at)) : '—';
?>
<tr class="igsp-post-row <?php echo !$is_active ? 'igsp-post-inactive' : ''; ?>"
data-post-id="<?php echo esc_attr($post->id); ?>">
<td class="igsp-col-thumb">
<?php if ($thumb_url): ?>
<img src="<?php echo esc_url($thumb_url); ?>" alt="" class="igsp-post-thumb">
<?php else: ?>
<div class="igsp-post-thumb-placeholder">
<span class="dashicons dashicons-format-image"></span>
</div>
<?php endif; ?>
</td>
<td class="igsp-col-caption">
<span class="igsp-caption-text">
<?php echo esc_html($caption_short); ?>
</span>
</td>
<td class="igsp-col-link">
<?php if (!empty($post->post_url)): ?>
<a href="<?php echo esc_url($post->post_url); ?>" target="_blank" rel="noopener noreferrer"
class="igsp-post-link" title="<?php echo esc_attr($post->post_url); ?>">
<span class="dashicons dashicons-external"></span>
</a>
<?php else: ?>
<span class="igsp-no-link">—</span>
<?php endif; ?>
</td>
<td class="igsp-col-type">
<?php if ($is_manual): ?>
<span class="igsp-type-badge igsp-type-manual">
<?php esc_html_e('Manual', 'instagram-gallery-sync-pro'); ?>
</span>
<?php else: ?>
<span class="igsp-type-badge igsp-type-synced">
<?php esc_html_e('Synced', 'instagram-gallery-sync-pro'); ?>
</span>
<?php endif; ?>
</td>
<td class="igsp-col-status">
<span
class="igsp-status-badge <?php echo $is_active ? 'igsp-status-active' : 'igsp-status-inactive'; ?>">
<?php echo $is_active ? esc_html__('Aktiv', 'instagram-gallery-sync-pro') : esc_html__('Inaktiv', 'instagram-gallery-sync-pro'); ?>
</span>
</td>
<td class="igsp-col-date">
<span class="igsp-post-date">
<?php echo esc_html($posted_date); ?>
</span>
</td>
<td class="igsp-col-actions">
<div class="igsp-action-buttons">
<button type="button" class="button button-small igsp-toggle-post"
data-post-id="<?php echo esc_attr($post->id); ?>"
title="<?php echo $is_active ? esc_attr__('Deaktivieren', 'instagram-gallery-sync-pro') : esc_attr__('Aktivieren', 'instagram-gallery-sync-pro'); ?>">
<span
class="dashicons <?php echo $is_active ? 'dashicons-hidden' : 'dashicons-visibility'; ?>"></span>
</button>
<button type="button" class="button button-small igsp-delete-post"
data-post-id="<?php echo esc_attr($post->id); ?>"
title="<?php esc_attr_e('Löschen', 'instagram-gallery-sync-pro'); ?>">
<span class="dashicons dashicons-trash"></span>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="igsp-pagination">
<?php
$base_url = admin_url('admin.php?page=' . IGSP_Admin::PAGE_SLUG . '&tab=posts');
if (!empty($search)) {
$base_url .= '&s=' . urlencode($search);
}
for ($i = 1; $i <= $total_pages; $i++):
$page_url = $base_url . '&paged=' . $i;
?>
<a href="<?php echo esc_url($page_url); ?>"
class="button <?php echo $current_page === $i ? 'button-primary' : ''; ?>">
<?php echo esc_html($i); ?>
</a>
<?php endfor; ?>
<span class="igsp-pagination-info">
<?php printf(
esc_html__('Seite %1$d von %2$d (%3$d Posts)', 'instagram-gallery-sync-pro'),
$current_page,
$total_pages,
$total_posts
); ?>
</span>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<script type="text/javascript">
jQuery(document).ready(function ($) {
// === Media Library Image Upload ===
var igspMediaFrame = null;
$('#igsp-select-image-btn').on('click', function (e) {
e.preventDefault();
if (typeof wp === 'undefined' || typeof wp.media !== 'function') {
alert('WordPress Media Library konnte nicht geladen werden. Bitte lade die Seite neu.');
return;
}
if (igspMediaFrame) {
igspMediaFrame.open();
return;
}
igspMediaFrame = wp.media({
title: '<?php echo esc_js(__('Bild auswählen', 'instagram-gallery-sync-pro')); ?>',
button: { text: '<?php echo esc_js(__('Bild verwenden', 'instagram-gallery-sync-pro')); ?>' },
multiple: false,
library: { type: 'image' }
});
igspMediaFrame.on('select', function () {
var attachment = igspMediaFrame.state().get('selection').first().toJSON();
$('#igsp-attachment-id').val(attachment.id);
var previewUrl = (attachment.sizes && attachment.sizes.medium) ? attachment.sizes.medium.url : attachment.url;
$('#igsp-preview-img').attr('src', previewUrl);
$('#igsp-image-preview').show();
$('#igsp-upload-placeholder').hide();
});
igspMediaFrame.open();
});
// Remove image
$('#igsp-remove-image').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
$('#igsp-attachment-id').val('');
$('#igsp-preview-img').attr('src', '');
$('#igsp-image-preview').hide();
$('#igsp-upload-placeholder').show();
});
// === Submit Manual Post ===
$('#igsp-add-manual-post').on('click', function () {
var $btn = $(this);
var $result = $('#igsp-manual-post-result');
var attachmentId = $('#igsp-attachment-id').val();
if (!attachmentId) {
$result.removeClass('success').addClass('error').text('Bitte wähle ein Bild aus.').show();
return;
}
$btn.prop('disabled', true);
$result.hide();
var postedAt = $('#igsp-manual-date').val();
if (postedAt) {
postedAt = postedAt.replace('T', ' ') + ':00';
}
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_add_manual_post',
nonce: igspAdmin.nonce,
attachment_id: attachmentId,
post_url: $('#igsp-manual-url').val(),
caption: $('#igsp-manual-caption').val(),
posted_at: postedAt
}, function (response) {
$result.removeClass('success error');
if (response.success) {
$result.addClass('success').text(response.data.message).show();
// Reset form
$('#igsp-attachment-id').val('');
$('#igsp-preview-img').attr('src', '');
$('#igsp-image-preview').hide();
$('#igsp-upload-placeholder').show();
$('#igsp-manual-url').val('');
$('#igsp-manual-caption').val('');
$('#igsp-manual-date').val('');
setTimeout(function () { location.reload(); }, 1500);
} else {
$result.addClass('error').text(response.data.message).show();
}
$btn.prop('disabled', false);
}).fail(function () {
$result.addClass('error').text('Ein Fehler ist aufgetreten.').show();
$btn.prop('disabled', false);
});
});
// === Toggle Post Status ===
$(document).on('click', '.igsp-toggle-post', function () {
var $btn = $(this);
var postId = $btn.data('post-id');
var $row = $btn.closest('.igsp-post-row');
$btn.prop('disabled', true);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_toggle_post',
nonce: igspAdmin.nonce,
post_id: postId
}, function (response) {
if (response.success) {
var $icon = $btn.find('.dashicons');
var $badge = $row.find('.igsp-status-badge');
if (response.data.is_active) {
$row.removeClass('igsp-post-inactive');
$icon.removeClass('dashicons-visibility').addClass('dashicons-hidden');
$badge.removeClass('igsp-status-inactive').addClass('igsp-status-active').text('Aktiv');
} else {
$row.addClass('igsp-post-inactive');
$icon.removeClass('dashicons-hidden').addClass('dashicons-visibility');
$badge.removeClass('igsp-status-active').addClass('igsp-status-inactive').text('Inaktiv');
}
}
$btn.prop('disabled', false);
}).fail(function () { $btn.prop('disabled', false); });
});
// === Delete Post ===
$(document).on('click', '.igsp-delete-post', function () {
if (!confirm('Diesen Post wirklich löschen?')) return;
var $btn = $(this);
var postId = $btn.data('post-id');
var $row = $btn.closest('.igsp-post-row');
$btn.prop('disabled', true);
$.post(igspAdmin.ajaxUrl, {
action: 'igsp_delete_post',
nonce: igspAdmin.nonce,
post_id: postId
}, function (response) {
if (response.success) {
$row.fadeOut(300, function () { $(this).remove(); });
} else {
$btn.prop('disabled', false);
}
}).fail(function () { $btn.prop('disabled', false); });
});
});
</script>

View File

@@ -47,6 +47,9 @@ class IGSP_Admin
add_action('wp_ajax_igsp_reset_data', array($this, 'ajax_reset_data'));
add_action('wp_ajax_igsp_get_sync_status', array($this, 'ajax_get_sync_status'));
add_action('wp_ajax_igsp_clear_logs', array($this, 'ajax_clear_logs'));
add_action('wp_ajax_igsp_delete_post', array($this, 'ajax_delete_post'));
add_action('wp_ajax_igsp_toggle_post', array($this, 'ajax_toggle_post'));
add_action('wp_ajax_igsp_add_manual_post', array($this, 'ajax_add_manual_post'));
// Add settings link to plugins page
add_filter('plugin_action_links_' . IGSP_PLUGIN_BASENAME, array($this, 'add_settings_link'));
@@ -173,18 +176,21 @@ class IGSP_Admin
'igsp-admin-style',
IGSP_PLUGIN_URL . 'admin/css/admin-style.css',
array(),
IGSP_VERSION
IGSP_VERSION . '.' . time()
);
// WordPress components
wp_enqueue_style('wp-color-picker');
// WordPress Media Library
wp_enqueue_media();
// Admin JS
wp_enqueue_script(
'igsp-admin-script',
IGSP_PLUGIN_URL . 'admin/js/admin-script.js',
array('jquery', 'wp-color-picker'),
IGSP_VERSION,
array('jquery', 'wp-color-picker', 'media-upload'),
IGSP_VERSION . '.' . time(),
true
);
@@ -193,7 +199,7 @@ class IGSP_Admin
'igsp-ajax-sync',
IGSP_PLUGIN_URL . 'admin/js/ajax-sync.js',
array('jquery'),
IGSP_VERSION,
IGSP_VERSION . '.' . time(),
true
);
@@ -207,8 +213,13 @@ class IGSP_Admin
'syncError' => __('Sync failed. Check logs for details.', 'instagram-gallery-sync-pro'),
'confirmReset' => __('Are you sure? This will delete ALL Instagram data including images. This action cannot be undone.', 'instagram-gallery-sync-pro'),
'confirmClearLogs' => __('Are you sure you want to clear all logs?', 'instagram-gallery-sync-pro'),
'confirmDeletePost' => __('Are you sure you want to delete this post?', 'instagram-gallery-sync-pro'),
'processing' => __('Processing...', 'instagram-gallery-sync-pro'),
'done' => __('Done!', 'instagram-gallery-sync-pro'),
'postAdded' => __('Post added successfully!', 'instagram-gallery-sync-pro'),
'postDeleted' => __('Post deleted.', 'instagram-gallery-sync-pro'),
'selectImage' => __('Select Image', 'instagram-gallery-sync-pro'),
'useImage' => __('Use this image', 'instagram-gallery-sync-pro'),
),
));
}
@@ -382,6 +393,10 @@ class IGSP_Admin
'title' => __('Instagram', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-instagram',
),
'posts' => array(
'title' => __('Posts', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-images-alt2',
),
'layout' => array(
'title' => __('Layout', 'instagram-gallery-sync-pro'),
'icon' => 'dashicons-grid-view',
@@ -400,4 +415,120 @@ class IGSP_Admin
),
);
}
/**
* AJAX: Delete single post
*
* @return void
*/
public function ajax_delete_post()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
$post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
if (!$post_id) {
wp_send_json_error(array('message' => __('Invalid post ID.', 'instagram-gallery-sync-pro')));
}
$database = new IGSP_Database();
$result = $database->delete_post($post_id);
if ($result) {
// Clear gallery cache
igsp()->clear_transients();
wp_send_json_success(array('message' => __('Post deleted successfully.', 'instagram-gallery-sync-pro')));
} else {
wp_send_json_error(array('message' => __('Failed to delete post.', 'instagram-gallery-sync-pro')));
}
}
/**
* AJAX: Toggle post active/inactive
*
* @return void
*/
public function ajax_toggle_post()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
$post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
if (!$post_id) {
wp_send_json_error(array('message' => __('Invalid post ID.', 'instagram-gallery-sync-pro')));
}
$database = new IGSP_Database();
$result = $database->toggle_post_active($post_id);
if ($result) {
$post = $database->get_post($post_id);
igsp()->clear_transients();
wp_send_json_success(array(
'message' => __('Post status updated.', 'instagram-gallery-sync-pro'),
'is_active' => (bool) $post->is_active,
));
} else {
wp_send_json_error(array('message' => __('Failed to update post status.', 'instagram-gallery-sync-pro')));
}
}
/**
* AJAX: Add manual post
*
* @return void
*/
public function ajax_add_manual_post()
{
check_ajax_referer('igsp_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Permission denied.', 'instagram-gallery-sync-pro')));
}
$attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
$post_url = isset($_POST['post_url']) ? esc_url_raw($_POST['post_url']) : '';
$caption = isset($_POST['caption']) ? wp_kses_post($_POST['caption']) : '';
$posted_at = isset($_POST['posted_at']) ? sanitize_text_field($_POST['posted_at']) : '';
if (!$attachment_id) {
wp_send_json_error(array('message' => __('Please select an image.', 'instagram-gallery-sync-pro')));
}
// Copy from Media Library to plugin upload directory
$image_handler = new IGSP_Image_Handler();
$image_result = $image_handler->save_from_media_library($attachment_id);
if (is_wp_error($image_result)) {
wp_send_json_error(array('message' => $image_result->get_error_message()));
}
// Insert into database
$database = new IGSP_Database();
$result = $database->insert_manual_post(array(
'image_local_path' => $image_result['local_path'],
'image_thumbnail_path' => $image_result['thumbnail_path'],
'post_url' => $post_url,
'caption' => $caption,
'posted_at' => $posted_at,
'file_size' => $image_result['file_size'],
'image_width' => $image_result['width'],
'image_height' => $image_result['height'],
));
if ($result) {
igsp()->clear_transients();
wp_send_json_success(array('message' => __('Post added successfully!', 'instagram-gallery-sync-pro')));
} else {
wp_send_json_error(array('message' => __('Failed to add post.', 'instagram-gallery-sync-pro')));
}
}
}

View File

@@ -24,7 +24,7 @@ class IGSP_Database
*
* @var string
*/
private $db_version = '1.0.0';
private $db_version = '1.1.0';
/**
* Constructor
@@ -64,6 +64,7 @@ class IGSP_Database
image_width INT(11) DEFAULT 0,
image_height INT(11) DEFAULT 0,
is_active TINYINT(1) DEFAULT 1,
is_manual TINYINT(1) DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY instagram_id (instagram_id),
KEY username (username),
@@ -250,6 +251,7 @@ class IGSP_Database
'order' => 'newest',
'active' => true,
'username' => '',
'search' => '',
);
$args = wp_parse_args($args, $defaults);
@@ -267,6 +269,13 @@ class IGSP_Database
$values[] = $args['username'];
}
if (!empty($args['search'])) {
$where[] = '(caption LIKE %s OR username LIKE %s)';
$search_term = '%' . $wpdb->esc_like($args['search']) . '%';
$values[] = $search_term;
$values[] = $search_term;
}
// Order
switch ($args['order']) {
case 'oldest':
@@ -315,6 +324,116 @@ class IGSP_Database
);
}
/**
* Count posts with optional filters
*
* @param array $args Filter arguments
* @return int
*/
public function count_posts_filtered($args = array())
{
global $wpdb;
$defaults = array(
'active' => null,
'search' => '',
);
$args = wp_parse_args($args, $defaults);
$where = array('1=1');
$values = array();
if ($args['active'] !== null) {
$where[] = 'is_active = %d';
$values[] = $args['active'] ? 1 : 0;
}
if (!empty($args['search'])) {
$where[] = '(caption LIKE %s OR username LIKE %s)';
$search_term = '%' . $wpdb->esc_like($args['search']) . '%';
$values[] = $search_term;
$values[] = $search_term;
}
$where_clause = implode(' AND ', $where);
$query = "SELECT COUNT(*) FROM " . IGSP_TABLE_POSTS . " WHERE {$where_clause}";
if (!empty($values)) {
$query = $wpdb->prepare($query, $values);
}
return (int) $wpdb->get_var($query);
}
/**
* Toggle post active status
*
* @param int $id Post ID
* @return bool
*/
public function toggle_post_active($id)
{
global $wpdb;
$post = $this->get_post($id);
if (!$post) {
return false;
}
$new_status = $post->is_active ? 0 : 1;
return $wpdb->update(
IGSP_TABLE_POSTS,
array('is_active' => $new_status),
array('id' => $id),
array('%d'),
array('%d')
) !== false;
}
/**
* Insert a manually added post
*
* @param array $data Post data
* @return int|false Insert ID or false on failure
*/
public function insert_manual_post($data)
{
global $wpdb;
$instagram_id = 'manual_' . time() . '_' . wp_rand(1000, 9999);
$insert_data = array(
'instagram_id' => $instagram_id,
'username' => sanitize_text_field($data['username'] ?? get_option('igsp_username', 'manual')),
'image_local_path' => isset($data['image_local_path']) ? sanitize_text_field($data['image_local_path']) : '',
'image_thumbnail_path' => isset($data['image_thumbnail_path']) ? sanitize_text_field($data['image_thumbnail_path']) : '',
'post_url' => isset($data['post_url']) ? esc_url_raw($data['post_url']) : '',
'caption' => isset($data['caption']) ? wp_kses_post($data['caption']) : '',
'likes_count' => 0,
'comments_count' => 0,
'posted_at' => isset($data['posted_at']) && !empty($data['posted_at']) ? sanitize_text_field($data['posted_at']) : current_time('mysql'),
'synced_at' => current_time('mysql'),
'file_size' => isset($data['file_size']) ? absint($data['file_size']) : 0,
'image_width' => isset($data['image_width']) ? absint($data['image_width']) : 0,
'image_height' => isset($data['image_height']) ? absint($data['image_height']) : 0,
'is_active' => 1,
'is_manual' => 1,
);
$result = $wpdb->insert(
IGSP_TABLE_POSTS,
$insert_data,
array(
'%s', '%s', '%s', '%s', '%s', '%s',
'%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%d'
)
);
return $result ? $wpdb->insert_id : false;
}
/**
* Delete post
*

View File

@@ -441,6 +441,66 @@ class IGSP_Image_Handler
return size_format($bytes);
}
/**
* Save image from WordPress Media Library to plugin upload directory
*
* @param int $attachment_id WordPress attachment ID
* @return array|WP_Error Array with paths or WP_Error
*/
public function save_from_media_library($attachment_id)
{
$file_path = get_attached_file($attachment_id);
if (!$file_path || !file_exists($file_path)) {
return new WP_Error('file_not_found', __('Attachment file not found.', 'instagram-gallery-sync-pro'));
}
// Validate
$validation = $this->validate_image($file_path);
if (is_wp_error($validation)) {
return $validation;
}
$file_info = $validation;
$extension = $file_info['extension'];
// Generate unique filename
$safe_id = 'manual_' . time() . '_' . wp_rand(1000, 9999);
$filename = $safe_id . '.' . $extension;
$dest_path = $this->upload_path . '/' . $filename;
// Copy file (don't move - keep original in Media Library)
$copied = copy($file_path, $dest_path);
if (!$copied) {
return new WP_Error('copy_failed', __('Failed to copy image file.', 'instagram-gallery-sync-pro'));
}
chmod($dest_path, 0644);
// Generate thumbnail
$thumbnail_path = $this->generate_thumbnail($dest_path, $safe_id, $extension);
// Get dimensions
$dimensions = getimagesize($dest_path);
$result = array(
'local_path' => IGSP_UPLOAD_DIR . '/' . $filename,
'thumbnail_path' => $thumbnail_path ?: '',
'full_url' => $this->upload_url . '/' . $filename,
'file_size' => filesize($dest_path),
'width' => $dimensions ? $dimensions[0] : 0,
'height' => $dimensions ? $dimensions[1] : 0,
);
$this->logger->info(
sprintf(__('Manual image saved: %s', 'instagram-gallery-sync-pro'), $filename),
array('size' => $result['file_size'])
);
return $result;
}
/**
* Clear all stored images
*

BIN
public/.DS_Store vendored

Binary file not shown.