“Stay a while and Listen”

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.

From https://half-life.fandom.com/wiki/Combine_Power_Generator

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

from https://en.wikipedia.org/wiki/Palazzo_Braschi

Leave a Reply