I added some visitor analytics to wordpress via a custom plugin

As a non-web programmer this was surprisingly easy…Only because I have AI.

Claude AI was able to throw everything into a script and threw it into a zip file and installed it into wordpress. A apart of me feels like I cheated but at the same time I don’t want to spend hours learning the wordpress database system, plugin api, and chart js just to see a few numbers and a line plot.
I’ve attached the code below that you can package into a .zip to upload to any wordpress site below.
Expand for plugin php code.
<?php
/**
* Plugin Name: Visitor Counter Dashboard
* Description: Displays visitor statistics in a graph on the admin dashboard
* Version: 1.3.0
* Author: Claude Ai (via Will Kolb)
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class VisitorAnalytics {
private $table_name;
public function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . 'visitor_analytics';
// Hook into WordPress
add_action('init', array($this, 'track_visitor'));
add_action('wp_dashboard_setup', array($this, 'add_dashboard_widget'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
add_action('wp_ajax_get_visitor_data', array($this, 'ajax_get_visitor_data'));
// Activation hook
register_activation_hook(__FILE__, array($this, 'create_table'));
}
/**
* Create database table on plugin activation
*/
public function create_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$this->table_name} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
visit_date date NOT NULL,
visit_count int(11) NOT NULL DEFAULT 1,
ip_address varchar(45) NOT NULL,
user_agent text,
page_url varchar(255),
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY unique_daily_ip (visit_date, ip_address)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
/**
* Track visitor on each page load
*/
public function track_visitor() {
// Don't track admin users or admin pages
if (is_admin() || current_user_can('manage_options')) {
return;
}
global $wpdb;
$ip_address = $this->get_user_ip();
$today = current_time('Y-m-d');
$user_agent = sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? '');
$page_url = sanitize_text_field($_SERVER['REQUEST_URI'] ?? '');
// Check if this IP has already been recorded today
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT id FROM {$this->table_name} WHERE visit_date = %s AND ip_address = %s",
$today, $ip_address
));
if (!$existing) {
// Insert new visitor record
$wpdb->insert(
$this->table_name,
array(
'visit_date' => $today,
'ip_address' => $ip_address,
'user_agent' => $user_agent,
'page_url' => $page_url,
'visit_count' => 1
),
array('%s', '%s', '%s', '%s', '%d')
);
}
}
/**
* Get user's IP address
*/
private function get_user_ip() {
$ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
foreach ($ip_keys as $key) {
if (array_key_exists($key, $_SERVER) === true) {
$ip = sanitize_text_field($_SERVER[$key]);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1');
}
/**
* Add dashboard widget
*/
public function add_dashboard_widget() {
wp_add_dashboard_widget(
'visitor_analytics_widget',
'Visitor Analytics',
array($this, 'display_dashboard_widget')
);
}
/**
* Display the dashboard widget content
*/
public function display_dashboard_widget() {
?>
<div id="visitor-analytics-container">
<div style="margin-bottom: 15px;">
<select id="analytics-period" style="margin-right: 10px;">
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<button id="refresh-analytics" class="button button-secondary">Refresh</button>
</div>
<div id="analytics-summary" style="display: flex; gap: 20px; margin-bottom: 20px;">
<div style="text-align: center;">
<h3 style="margin: 0; color: #23282d;">Today</h3>
<p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #0073aa;" id="today-visitors">-</p>
</div>
<div style="text-align: center;">
<h3 style="margin: 0; color: #23282d;">This Week</h3>
<p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #00a32a;" id="week-visitors">-</p>
</div>
<div style="text-align: center;">
<h3 style="margin: 0; color: #23282d;">This Month</h3>
<p style="font-size: 24px; font-weight: bold; margin: 5px 0; color: #d63638;" id="month-visitors">-</p>
</div>
</div>
<canvas id="visitor-chart" width="400" height="200" style="max-height: 200px;"></canvas>
<div id="analytics-loading" style="text-align: center; padding: 20px;">Loading...</div>
</div>
<script>
jQuery(document).ready(function($) {
let chart = null;
// Cleanup function to properly destroy chart
function destroyChart() {
if (chart && typeof chart.destroy === 'function') {
chart.destroy();
chart = null;
}
}
// Clean up when leaving the page
$(window).on('beforeunload', destroyChart);
function loadAnalytics() {
const period = $('#analytics-period').val();
$('#analytics-loading').show();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'get_visitor_data',
period: period,
nonce: '<?php echo wp_create_nonce('visitor_analytics_nonce'); ?>'
},
success: function(response) {
if (response.success) {
updateSummary(response.data.summary);
updateChart(response.data.chart_data);
}
$('#analytics-loading').hide();
},
error: function() {
$('#analytics-loading').hide();
alert('Error loading analytics data');
}
});
}
function updateSummary(summary) {
$('#today-visitors').text(summary.today || 0);
$('#week-visitors').text(summary.week || 0);
$('#month-visitors').text(summary.month || 0);
}
function updateChart(chartData) {
const canvas = document.getElementById('visitor-chart');
const ctx = canvas.getContext('2d');
// Properly destroy existing chart
if (chart) {
chart.destroy();
chart = null;
}
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Reset canvas size
canvas.style.width = '100%';
canvas.style.height = '200px';
chart = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.labels,
datasets: [{
label: 'Daily Visitors',
data: chartData.data,
borderColor: '#0073aa',
backgroundColor: 'rgba(0, 115, 170, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
cornerRadius: 4
}
},
elements: {
point: {
radius: 3,
hoverRadius: 6
}
}
}
});
}
// Event listeners
$('#analytics-period').change(loadAnalytics);
$('#refresh-analytics').click(loadAnalytics);
// Initial load
loadAnalytics();
});
</script>
<?php
}
/**
* Enqueue admin scripts
*/
public function enqueue_admin_scripts($hook) {
if ($hook === 'index.php') {
wp_enqueue_script('chart-js', 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js', array(), '3.9.1', true);
}
}
/**
* AJAX handler for getting visitor data
*/
public function ajax_get_visitor_data() {
// Verify nonce
if (!wp_verify_nonce($_POST['nonce'], 'visitor_analytics_nonce')) {
wp_die('Security check failed');
}
// Check user permissions
if (!current_user_can('manage_options')) {
wp_die('Insufficient permissions');
}
global $wpdb;
$period = intval($_POST['period']);
// Get summary data
$today = current_time('Y-m-d');
$week_ago = date('Y-m-d', strtotime('-7 days', current_time('timestamp')));
$month_ago = date('Y-m-d', strtotime('-30 days', current_time('timestamp')));
$today_count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date = %s", $today
));
$week_count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s", $week_ago
));
$month_count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s", $month_ago
));
// Get chart data
$start_date = date('Y-m-d', strtotime("-{$period} days", current_time('timestamp')));
$chart_data = $wpdb->get_results($wpdb->prepare(
"SELECT visit_date, COUNT(*) as visitor_count
FROM {$this->table_name}
WHERE visit_date >= %s
GROUP BY visit_date
ORDER BY visit_date ASC",
$start_date
));
// Fill in missing dates with zero counts
$labels = array();
$data = array();
for ($i = $period - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-{$i} days", current_time('timestamp')));
$labels[] = date('M j', strtotime($date));
$count = 0;
foreach ($chart_data as $row) {
if ($row->visit_date === $date) {
$count = intval($row->visitor_count);
break;
}
}
$data[] = $count;
}
wp_send_json_success(array(
'summary' => array(
'today' => intval($today_count),
'week' => intval($week_count),
'month' => intval($month_count)
),
'chart_data' => array(
'labels' => $labels,
'data' => $data
)
));
}
}
// Initialize the plugin
new VisitorAnalytics();
In gamedev news I want to make a “boss” area thing that the player has to destroy
I think I want to incorporate that helix thingy as the “power core” (see: this post).
I’ve made a few iterations but they keep looking like the Red power ranger’s vape pen:

My hope was to make an equivalent to the Doom 2016 gore nest or the half life 2 energy pylon things, so that the player would essentially break the core causing some gameplay rush event where you gotta kill a bunch of bots and run.

I might go with some kind of oppressive robotic face instead? Like the Mussolini face from WWII.
