From 75a097a018a0090f5902758353c578fce4aa2a25 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 23 May 2026 18:43:42 +0000
Subject: [PATCH] =CustomBlocks.php overhaul relatively complete. Also refactored the gallery in gallery.min.js and the jvbRenderGallery.
---
inc/managers/AdminPages.php | 613 ++++++++++++------------------------------------------
1 files changed, 141 insertions(+), 472 deletions(-)
diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index 0ae75d4..5878e17 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -1,7 +1,8 @@
<?php
namespace JVBase\managers;
-use JVBase\utility\Features;
+use JVBase\registrar\Registrar;
+use JVBase\base\Site;
use WP_REST_Response;
if (!defined('ABSPATH')) {
@@ -31,7 +32,7 @@
'menu_title' => 'JakeVan',
'capability' => 'manage_options',
'menu_slug' => BASE . 'settings',
- 'icon' => jvbCSSIcon('settings'),
+ 'icon' => jvbCSSIcon('gear-six'),
'position' => 0
];
$this->subpages = get_option(BASE.'adminSubpage', []);
@@ -47,163 +48,11 @@
add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
- add_action('rest_api_init', [$this, 'registerRestRoutes']);
// Handle form submissions
add_action('admin_init', [$this, 'handleAdminPageSubmission']);
add_action('admin_notices', [$this, 'displayAdminNotices']);
}
- /**
- * Register REST API routes for admin actions
- */
- public function registerRestRoutes(): void
- {
- register_rest_route('jvb/v1', '/admin-cache', [
- 'methods' => 'POST',
- 'callback' => [$this, 'handleCacheAction'],
- 'permission_callback' => [$this, 'checkAdminPermission']
- ]);
-
- register_rest_route('jvb/v1', '/admin-icons', [
- 'methods' => 'POST',
- 'callback' => [$this, 'handleIconAction'],
- 'permission_callback' => [$this, 'checkAdminPermission']
- ]);
- }
-
- /**
- * Check if user has admin permissions
- */
- public function checkAdminPermission(\WP_REST_Request $request): bool
- {
- if (!current_user_can('manage_options')) {
- return false;
- }
-
- // Verify nonce
- $nonce = $request->get_header('X-WP-Nonce');
- if (!wp_verify_nonce($nonce, 'wp_rest')) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Handle cache-related actions
- */
- public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
- {
- $action = sanitize_text_field($request->get_param('action'));
-
- switch ($action) {
- case 'flush-all':
- wp_cache_flush();
- return new \WP_REST_Response([
- 'success' => true,
- 'message' => 'All caches flushed successfully'
- ]);
-
- case 'flush-cache':
- $group = sanitize_text_field($request->get_param('group'));
- if (empty($group)) {
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'No cache group specified'
- ], 400);
- }
-
- \JVBase\managers\CacheManager::invalidateAll($group);
-
- return new \WP_REST_Response([
- 'success' => true,
- 'message' => "Cache group '{$group}' flushed successfully"
- ]);
-
- default:
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'Invalid action'
- ], 400);
- }
- }
-
- /**
- * Handle icon-related actions
- */
- public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
- {
- $action = sanitize_text_field($request->get_param('action'));
- $icons = \JVBase\managers\IconsManager::getInstance();
-
- switch ($action) {
- case 'refresh-icons':
- $icons->forceRefresh();
- return new \WP_REST_Response([
- 'success' => true,
- 'message' => 'Icon CSS regenerated successfully'
- ]);
-
- case 'restore-icon-version':
- $timestamp = (int)$request->get_param('timestamp');
- if (empty($timestamp)) {
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'No timestamp provided'
- ], 400);
- }
-
- if ($icons->restoreVersion($timestamp)) {
- return new \WP_REST_Response([
- 'success' => true,
- 'message' => 'Icon version restored successfully'
- ]);
- }
-
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'Failed to restore icon version'
- ], 500);
-
- case 'merge-icon-versions':
- $timestamps = $request->get_param('timestamps');
-
- if (empty($timestamps) || !is_array($timestamps)) {
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'No versions selected for merging'
- ], 400);
- }
-
- $timestamps = array_map('intval', $timestamps);
-
- if (count($timestamps) < 2) {
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'Please select at least 2 versions to merge'
- ], 400);
- }
-
- if ($icons->mergeVersions($timestamps)) {
- return new \WP_REST_Response([
- 'success' => true,
- 'message' => 'Icon versions merged successfully'
- ]);
- }
-
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'Failed to merge icon versions'
- ], 500);
-
- default:
- return new \WP_REST_Response([
- 'success' => false,
- 'message' => 'Invalid action'
- ], 400);
- }
- }
-
/**
* Register a subpage to appear under the main settings page
*
@@ -369,6 +218,11 @@
}
}
+ public function getMainConfig():array
+ {
+ return $this->main_page;
+ }
+
/**
* Render the main settings page
*/
@@ -437,7 +291,7 @@
protected function renderStatusItems():void
{
// Get queue stats
- $queue_status = JVB()->queue()->getQueueStatus();
+ $queue_status = JVB()->queue()->getStatus();
error_log('Queue Status: '.print_r($queue_status, true));
// Other system checks
@@ -464,11 +318,11 @@
</li>
<li>
<span class="status-label">Content Types:</span>
- <span class="status-value"><?= count(JVB_CONTENT); ?> registered</span>
+ <span class="status-value"><?= count(Registrar::getRegistered('post')); ?> registered</span>
</li>
<li>
<span class="status-label">Taxonomies:</span>
- <span class="status-value"><?= count(JVB_TAXONOMY); ?> registered</span>
+ <span class="status-value"><?= count(Registrar::getRegistered('term')); ?> registered</span>
</li>
<?php
}
@@ -500,10 +354,8 @@
global $wpdb;
$week_ago = date('Y-m-d H:i:s', strtotime('-7 days'));
- $content_types = [];
- foreach (JVB_CONTENT as $content => $config) {
- $content_types[jvbCheckBase($content)] = $config['plural'];
- }
+ $content_types = array_map(function ($type) { return jvbCheckBase($type); },
+ Registrar::getRegistered('post'));
?>
<table class="jvb-content-table">
@@ -582,9 +434,9 @@
if (current_user_can($action['capability'])) {
?>
<a data-action="<?=$action['slug']?>" class="jvb-action">
- <?= jvbIcon($action['icon']); ?>
+ <?= jvbDashIcon($action['icon']); ?>
<span class="jvb-link-title"><?= esc_html($action['label'])?></span>
- <span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span>
+ <span class="loader"><?=jvbDashIcon('arrows-clockwise')?><?=jvbDashIcon('check')?></span>
</a>
<?php
}
@@ -596,40 +448,41 @@
*
* @param string $hook Current admin page
*/
- public function enqueueAdminAssets(string $hook):void
- {
- // Check if we're on an Edmonton Ink admin page
- if (strpos($hook, BASE) === false) {
- return;
- }
+ public function enqueueAdminAssets(string $hook):void
+ {
+ // More robust check for JVB admin pages
+ if (strpos($hook, BASE) === false) {
+ return;
+ }
- // Enqueue admin styles
- wp_enqueue_style(
- 'jvb-admin-styles',
- JVB_URL . 'assets/css/admin.css',
- [],
- '1.0.0'
- );
+ // Enqueue admin styles
+ wp_enqueue_style(
+ 'jvb-admin-styles',
+ JVB_URL . 'assets/css/admin.css',
+ [],
+ '1.1'
+ );
- // Enqueue admin scripts
- wp_enqueue_script(
- 'jvb-admin-scripts',
- JVB_URL . 'assets/js/admin.js',
- [],
- '1.0.0',
- true
- );
+ // Enqueue admin scripts - make sure jvb-auth is loaded first
+ wp_enqueue_script(
+ 'jvb-admin-scripts',
+ JVB_URL . 'assets/js/admin.js',
+ ['jvb-auth'],
+ '1.1',
+ ['strategy' => 'defer', 'in_footer' => true]
+ );
- wp_localize_script(
- 'jvb-admin-scripts',
- 'jvbSettings',
- [
- 'api' => rest_url('jvb/v1/admin-action'),
- 'nonce' => wp_create_nonce('wp_rest'),
- 'action' => wp_create_nonce('itsme'),
- ]
- );
- }
+ // Localize to jvb-admin-scripts as well for redundancy
+ wp_localize_script(
+ 'jvb-auth',
+ 'jvbSettings',
+ [
+ 'api' => rest_url('jvb/v1/'),
+ 'nonce' => wp_create_nonce('wp_rest'),
+ 'action' => wp_create_nonce('itsme'),
+ ]
+ );
+ }
/**
* Create a custom SVG icon for the admin menu
@@ -639,7 +492,7 @@
*/
protected function getIcon(string $icon = 'logo', bool $css = false): string
{
- $svg = jvbIcon($icon, ['wrap' => false]);
+ $svg = jvbDashIcon($icon, ['wrap' => false]);
if ($css) {
// For CSS, replace currentColor with brand color
$svg = str_replace('currentColor', '#FF0080', $svg);
@@ -658,20 +511,17 @@
public function renderCachePage():void
{
- $connections = CacheManager::getAllConnections();
+ $groups = Cache::getAllGroups();
- // Separate generic vs. specific caches
- $generic_groups = [];
- $content_specific = [];
- $nonce = wp_create_nonce('wp_rest');
+ // Separate by type
+ $generic = [];
+ $specific = [];
- foreach ($connections as $group => $configs) {
- $is_generic = !$this->isBoundToContentOrTaxonomy($group);
-
- if ($is_generic) {
- $generic_groups[$group] = $configs;
+ foreach ($groups as $group => $data) {
+ if ($this->isBoundToContentOrTaxonomy($group)) {
+ $specific[$group] = $data;
} else {
- $content_specific[$group] = $configs;
+ $generic[$group] = $data;
}
}
@@ -680,8 +530,10 @@
<h1>Cache Management</h1>
<div class="jvb-cache-actions">
- <button type="button" class="button button-primary" data-action="flush-all">
- <?= jvbIcon('arrows-clockwise'); ?>
+ <button type="button"
+ class="button button-primary"
+ data-cache-action="flush-all">
+ <?= jvbDashIcon('arrows-clockwise'); ?>
Flush All Caches
</button>
</div>
@@ -697,16 +549,19 @@
</tr>
</thead>
<tbody>
- <?php if (empty($generic_groups)): ?>
+ <?php if (empty($generic)): ?>
<tr><td colspan="3">No generic caches registered</td></tr>
<?php else: ?>
- <?php foreach ($generic_groups as $group => $configs): ?>
+ <?php foreach ($generic as $group => $data): ?>
<tr>
<td><strong><?= esc_html($group); ?></strong></td>
- <td><?= $this->formatConnections($configs); ?></td>
+ <td><?= $this->formatConnections($data); ?></td>
<td>
- <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
- <?= jvbIcon('trash'); ?> Flush
+ <button type="button"
+ class="button"
+ data-cache-action="flush-cache"
+ data-group="<?= esc_attr($group); ?>">
+ <?= jvbDashIcon('trash'); ?> Flush
</button>
</td>
</tr>
@@ -727,13 +582,16 @@
</tr>
</thead>
<tbody>
- <?php foreach ($content_specific as $group => $configs): ?>
+ <?php foreach ($specific as $group => $data): ?>
<tr>
<td><strong><?= esc_html($group); ?></strong></td>
- <td><?= $this->formatConnections($configs); ?></td>
+ <td><?= $this->formatConnections($data); ?></td>
<td>
- <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
- <?= jvbIcon('trash'); ?> Flush
+ <button type="button"
+ class="button"
+ data-cache-action="flush-cache"
+ data-group="<?= esc_attr($group); ?>">
+ <?= jvbDashIcon('trash'); ?> Flush
</button>
</td>
</tr>
@@ -742,57 +600,6 @@
</table>
</details>
</div>
- <script>
- (function() {
- const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-cache')); ?>';
- const nonce = '<?= esc_js($nonce); ?>';
-
- function callCacheAction(action, data = {}) {
- const body = { action, ...data };
-
- return fetch(apiUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-WP-Nonce': nonce
- },
- body: JSON.stringify(body)
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- alert(data.message || 'Success!');
- location.reload();
- } else {
- alert('Error: ' + (data.message || 'Unknown error'));
- }
- })
- .catch(error => {
- alert('Network error: ' + error.message);
- console.error('Error:', error);
- });
- }
-
- // Flush all caches
- document.querySelector('[data-action="flush-all"]')?.addEventListener('click', function() {
- if (confirm('Flush all caches? This may temporarily slow down your site.')) {
- this.disabled = true;
- callCacheAction('flush-all');
- }
- });
-
- // Flush individual cache groups
- document.querySelectorAll('[data-action="flush-cache"]').forEach(btn => {
- btn.addEventListener('click', function() {
- const group = this.getAttribute('data-group');
- if (confirm(`Flush cache group "${group}"?`)) {
- this.disabled = true;
- callCacheAction('flush-cache', { group: group });
- }
- });
- });
- })();
- </script>
<?php
}
@@ -800,34 +607,33 @@
{
$group = jvbNoBase($group);
- if (defined('JVB_CONTENT')) {
- foreach (JVB_CONTENT as $key => $config) {
- if (jvbNoBase($key) === $group) {
- return true;
- }
+ $registered = Registrar::getRegistered();
+ foreach ($registered as $r) {
+ if ($r === $group) {
+ return true;
}
}
-
- if (defined('JVB_TAXONOMY')) {
- foreach (JVB_TAXONOMY as $key => $config) {
- if (jvbNoBase($key) === $group) {
- return true;
- }
- }
- }
-
return false;
}
- protected function formatConnections(array $configs): string
+ protected function formatConnections(array $data): string
{
- $connections = [];
- foreach ($configs as $config) {
- $parent = $config['parent'] ?? 'unknown';
- $scope = $config['scope'] ?? 'id';
- $connections[] = "{$parent} ({$scope})";
+ $parts = [];
+
+ if (!empty($data['connects_to'])) {
+ $targets = array_map(function($conn) {
+ $flush_text = $conn['flush'] ? ' (flush all)' : '';
+ return $conn['group'] . $flush_text;
+ }, $data['connects_to']);
+ $parts[] = '<strong>Invalidates:</strong> ' . implode(', ', $targets);
}
- return esc_html(implode(', ', $connections));
+
+ if (!empty($data['connected_from'])) {
+ $sources = array_map(fn($conn) => $conn['group'], $data['connected_from']);
+ $parts[] = '<strong>Invalidated by:</strong> ' . implode(', ', $sources);
+ }
+
+ return $parts ? implode('<br>', $parts) : 'No connections';
}
public function handleCacheActions($response, $request, $action):WP_REST_Response
@@ -853,7 +659,8 @@
], 400);
}
- \JVBase\managers\CacheManager::invalidateAll($group);
+ $group = sanitize_text_field($request->get_param('group'));
+ Cache::invalidateGroup($group);
return new WP_REST_Response([
'success' => true,
@@ -861,93 +668,59 @@
]);
}
- if ($action === 'merge-icon-versions') {
- $timestamps = $request->get_param('timestamps');
-
- if (empty($timestamps) || !is_array($timestamps)) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'No versions selected for merging'
- ], 400);
- }
-
- // Convert to integers
- $timestamps = array_map('intval', $timestamps);
-
- if (count($timestamps) < 2) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Please select at least 2 versions to merge'
- ], 400);
- }
-
- $icons = \JVBase\managers\IconsManager::getInstance();
-
- if ($icons->mergeVersions($timestamps)) {
- return new WP_REST_Response([
- 'success' => true,
- 'message' => 'Icon versions merged successfully'
- ]);
- }
-
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Failed to merge icon versions'
- ], 500);
- }
-
- if ($action === 'refresh-icons') {
- $icons = \JVBase\managers\IconsManager::getInstance();
- $icons->forceRefresh();
-
- return new WP_REST_Response([
- 'success' => true,
- 'message' => 'Icon CSS refresh triggered'
- ]);
- }
-
- if ($action === 'restore-icon-version') {
- $timestamp = (int)$request->get_param('timestamp');
- $icons = \JVBase\managers\IconsManager::getInstance();
-
- if ($icons->restoreVersion($timestamp)) {
- return new WP_REST_Response([
- 'success' => true,
- 'message' => 'Icon version restored successfully'
- ]);
- }
-
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Failed to restore icon version'
- ], 500);
- }
-
return $response;
}
public function renderIconsPage():void
{
- $icons = \JVBase\managers\IconsManager::getInstance();
+ // Get current source from query param or default to 'icons'
+ $current_source = $_GET['icon_source'] ?? 'icons';
+ $current_source = sanitize_text_field($current_source);
+
+ // Get all registered icon sources
+ $all_sources = ['icons', 'forms', 'dash'];
+
+ $icons = IconsManager::for($current_source);
$versions = $icons->getVersionHistory();
- $nonce = wp_create_nonce('wp_rest');
?>
<div class="wrap jvb-admin-wrap">
<h1>Icon Management</h1>
+ <!-- Source Selector -->
+ <div class="jvb-icon-source-selector">
+ <label for="icon-source-select">Icon Source:</label>
+ <select id="icon-source-select"
+ onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
+ <?php foreach ($all_sources as $source): ?>
+ <option value="<?= esc_attr($source); ?>"
+ <?= selected($current_source, $source, false); ?>>
+ <?= esc_html(ucfirst($source)); ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+
<div class="jvb-icon-actions">
- <button type="button" class="button button-primary" data-action="refresh-icons">
- <?= jvbIcon('arrows-clockwise'); ?>
+ <button type="button"
+ class="button button-primary"
+ data-icon-action="refresh-icons"
+ data-source="<?= esc_attr($current_source); ?>">
+ <?= jvbDashIcon('arrows-clockwise'); ?>
Force Refresh CSS
</button>
- <button type="button" class="button" data-action="merge-icon-versions" id="merge-versions-btn" disabled>
- <?= jvbIcon('git-merge'); ?>
+ <button type="button"
+ class="button"
+ data-icon-action="merge-icon-versions"
+ data-source="<?= esc_attr($current_source); ?>"
+ id="merge-versions-btn"
+ disabled>
+ <?= jvbDashIcon('git-merge'); ?>
Merge Selected Versions
</button>
</div>
- <h2>Version History</h2>
+ <h2>Version History for <?= esc_html(ucfirst($current_source)); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
@@ -977,22 +750,25 @@
<td>
<?= esc_html($version['icon_count']); ?> icons
<button type="button"
- class="button-link"
- data-action="view-icon-list"
+ class="button-link view-icon-list-btn"
data-timestamp="<?= esc_attr($version['timestamp']); ?>">
(view)
</button>
</td>
<td><?= esc_html($version['size_formatted']); ?></td>
<td>
- <button type="button" class="button"
- data-action="restore-icon-version"
+ <button type="button"
+ class="button restore-version-btn"
+ data-icon-action="restore-icon-version"
+ data-source="<?= esc_attr($current_source); ?>"
data-timestamp="<?= esc_attr($version['timestamp']); ?>">
- <?= jvbIcon('arrow-counter-clockwise'); ?> Restore
+ <?= jvbDashIcon('arrow-counter-clockwise'); ?> Restore
</button>
</td>
</tr>
- <tr id="icon-list-<?= esc_attr($version['timestamp']); ?>" class="icon-list-row" style="display: none;">
+ <tr id="icon-list-<?= esc_attr($version['timestamp']); ?>"
+ class="icon-list-row"
+ style="display: none;">
<td colspan="5">
<div class="icon-list-content">
<?php foreach ($version['iconList'] as $style => $icons): ?>
@@ -1007,113 +783,6 @@
</tbody>
</table>
</div>
-
- <script>
- (function() {
- const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
- const nonce = '<?= esc_js($nonce); ?>';
-
- // Helper function for API calls
- function callIconAction(action, data = {}) {
- const body = { action, ...data };
-
- return fetch(apiUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-WP-Nonce': nonce
- },
- body: JSON.stringify(body)
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- alert(data.message || 'Success!');
- location.reload();
- } else {
- alert('Error: ' + (data.message || 'Unknown error'));
- }
- return data;
- })
- .catch(error => {
- alert('Network error: ' + error.message);
- console.error('Error:', error);
- });
- }
-
- // Enable/disable merge button based on selection
- document.querySelectorAll('.version-checkbox').forEach(checkbox => {
- checkbox.addEventListener('change', function() {
- const checkedCount = document.querySelectorAll('.version-checkbox:checked').length;
- document.getElementById('merge-versions-btn').disabled = checkedCount < 2;
- });
- });
-
- // Select all functionality
- const selectAll = document.getElementById('select-all-versions');
- if (selectAll) {
- selectAll.addEventListener('change', function() {
- document.querySelectorAll('.version-checkbox').forEach(checkbox => {
- checkbox.checked = this.checked;
- checkbox.dispatchEvent(new Event('change'));
- });
- });
- }
-
- // Toggle icon list view
- document.querySelectorAll('.view-icon-list-btn').forEach(btn => {
- btn.addEventListener('click', function() {
- const timestamp = this.getAttribute('data-timestamp');
- const row = document.getElementById('icon-list-' + timestamp);
- if (row) {
- row.style.display = row.style.display === 'none' ? '' : 'none';
- }
- });
- });
-
- // Force refresh button
- const refreshBtn = document.getElementById('refresh-icons-btn');
- if (refreshBtn) {
- refreshBtn.addEventListener('click', function() {
- if (confirm('Force regenerate icon CSS? This will reload the page.')) {
- this.disabled = true;
- callIconAction('refresh-icons');
- }
- });
- }
-
- // Merge versions button
- const mergeBtn = document.getElementById('merge-versions-btn');
- if (mergeBtn) {
- mergeBtn.addEventListener('click', function() {
- const checkboxes = document.querySelectorAll('.version-checkbox:checked');
- const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
-
- if (timestamps.length < 2) {
- alert('Please select at least 2 versions to merge');
- return;
- }
-
- if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
- this.disabled = true;
- callIconAction('merge-icon-versions', { timestamps: timestamps });
- }
- });
- }
-
- // Restore version buttons
- document.querySelectorAll('.restore-version-btn').forEach(btn => {
- btn.addEventListener('click', function() {
- const timestamp = parseInt(this.getAttribute('data-timestamp'));
-
- if (confirm('Restore this icon version? This will reload the page.')) {
- this.disabled = true;
- callIconAction('restore-icon-version', { timestamp: timestamp });
- }
- });
- });
- })();
- </script>
<?php
}
--
Gitblit v1.10.0