{"id":972,"date":"2025-05-26T09:28:13","date_gmt":"2025-05-26T13:28:13","guid":{"rendered":"https:\/\/willkolb.com\/?p=972"},"modified":"2025-05-26T09:28:13","modified_gmt":"2025-05-26T13:28:13","slug":"stay-a-while-and-listen","status":"publish","type":"post","link":"https:\/\/willkolb.com\/?p=972","title":{"rendered":"&#8220;Stay a while and Listen&#8221;"},"content":{"rendered":"\n<p>I added some visitor analytics to wordpress via a custom plugin<br><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"451\" height=\"425\" src=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-15.png\" alt=\"\" class=\"wp-image-973\" srcset=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-15.png 451w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-15-300x283.png 300w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-15-318x300.png 318w\" sizes=\"auto, (max-width: 451px) 100vw, 451px\" \/><\/figure>\n\n\n\n<p>As a non-web programmer this was surprisingly easy&#8230;Only because I have AI.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"508\" src=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17-1024x508.png\" alt=\"\" class=\"wp-image-977\" srcset=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17-1024x508.png 1024w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17-300x149.png 300w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17-768x381.png 768w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17-1536x762.png 1536w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17-500x248.png 500w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-17.png 1864w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>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&#8217;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.<\/p>\n\n\n\n<p>I&#8217;ve attached the code below that you can package into a .zip to upload to any wordpress site below.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Expand for plugin php code.<\/summary>\n<div class=\"wp-block-group is-nowrap is-layout-flex wp-container-core-group-is-layout-ad2f72ca wp-block-group-is-layout-flex\">\n<pre class=\"wp-block-code\"><code>&lt;?php\n\/**\n * Plugin Name: Visitor Counter Dashboard\n * Description: Displays visitor statistics in a graph on the admin dashboard\n * Version: 1.3.0\n * Author: Claude Ai (via Will Kolb)\n *\/\n\n\/\/ Prevent direct access\nif (!defined('ABSPATH')) {\n    exit;\n}\n\nclass VisitorAnalytics {\n    \n    private $table_name;\n    \n    public function __construct() {\n        global $wpdb;\n        $this->table_name = $wpdb->prefix . 'visitor_analytics';\n        \n        \/\/ Hook into WordPress\n        add_action('init', array($this, 'track_visitor'));\n        add_action('wp_dashboard_setup', array($this, 'add_dashboard_widget'));\n        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));\n        add_action('wp_ajax_get_visitor_data', array($this, 'ajax_get_visitor_data'));\n        \n        \/\/ Activation hook\n        register_activation_hook(__FILE__, array($this, 'create_table'));\n    }\n    \n    \/**\n     * Create database table on plugin activation\n     *\/\n    public function create_table() {\n        global $wpdb;\n        \n        $charset_collate = $wpdb->get_charset_collate();\n        \n        $sql = \"CREATE TABLE {$this->table_name} (\n            id mediumint(9) NOT NULL AUTO_INCREMENT,\n            visit_date date NOT NULL,\n            visit_count int(11) NOT NULL DEFAULT 1,\n            ip_address varchar(45) NOT NULL,\n            user_agent text,\n            page_url varchar(255),\n            created_at datetime DEFAULT CURRENT_TIMESTAMP,\n            PRIMARY KEY (id),\n            UNIQUE KEY unique_daily_ip (visit_date, ip_address)\n        ) $charset_collate;\";\n        \n        require_once(ABSPATH . 'wp-admin\/includes\/upgrade.php');\n        dbDelta($sql);\n    }\n    \n    \/**\n     * Track visitor on each page load\n     *\/\n    public function track_visitor() {\n        \/\/ Don't track admin users or admin pages\n        if (is_admin() || current_user_can('manage_options')) {\n            return;\n        }\n        \n        global $wpdb;\n        \n        $ip_address = $this->get_user_ip();\n        $today = current_time('Y-m-d');\n        $user_agent = sanitize_text_field($_SERVER&#91;'HTTP_USER_AGENT'] ?? '');\n        $page_url = sanitize_text_field($_SERVER&#91;'REQUEST_URI'] ?? '');\n        \n        \/\/ Check if this IP has already been recorded today\n        $existing = $wpdb->get_row($wpdb->prepare(\n            \"SELECT id FROM {$this->table_name} WHERE visit_date = %s AND ip_address = %s\",\n            $today, $ip_address\n        ));\n        \n        if (!$existing) {\n            \/\/ Insert new visitor record\n            $wpdb->insert(\n                $this->table_name,\n                array(\n                    'visit_date' => $today,\n                    'ip_address' => $ip_address,\n                    'user_agent' => $user_agent,\n                    'page_url' => $page_url,\n                    'visit_count' => 1\n                ),\n                array('%s', '%s', '%s', '%s', '%d')\n            );\n        }\n    }\n    \n    \/**\n     * Get user's IP address\n     *\/\n    private function get_user_ip() {\n        $ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');\n        \n        foreach ($ip_keys as $key) {\n            if (array_key_exists($key, $_SERVER) === true) {\n                $ip = sanitize_text_field($_SERVER&#91;$key]);\n                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {\n                    return $ip;\n                }\n            }\n        }\n        \n        return sanitize_text_field($_SERVER&#91;'REMOTE_ADDR'] ?? '127.0.0.1');\n    }\n    \n    \/**\n     * Add dashboard widget\n     *\/\n    public function add_dashboard_widget() {\n        wp_add_dashboard_widget(\n            'visitor_analytics_widget',\n            'Visitor Analytics',\n            array($this, 'display_dashboard_widget')\n        );\n    }\n    \n    \/**\n     * Display the dashboard widget content\n     *\/\n    public function display_dashboard_widget() {\n        ?>\n        &lt;div id=\"visitor-analytics-container\">\n            &lt;div style=\"margin-bottom: 15px;\">\n                &lt;select id=\"analytics-period\" style=\"margin-right: 10px;\">\n                    &lt;option value=\"7\">Last 7 days&lt;\/option>\n                    &lt;option value=\"30\">Last 30 days&lt;\/option>\n                    &lt;option value=\"90\">Last 90 days&lt;\/option>\n                &lt;\/select>\n                &lt;button id=\"refresh-analytics\" class=\"button button-secondary\">Refresh&lt;\/button>\n            &lt;\/div>\n            \n            &lt;div id=\"analytics-summary\" style=\"display: flex; gap: 20px; margin-bottom: 20px;\">\n                &lt;div style=\"text-align: center;\">\n                    &lt;h3 style=\"margin: 0; color: #23282d;\">Today&lt;\/h3>\n                    &lt;p style=\"font-size: 24px; font-weight: bold; margin: 5px 0; color: #0073aa;\" id=\"today-visitors\">-&lt;\/p>\n                &lt;\/div>\n                &lt;div style=\"text-align: center;\">\n                    &lt;h3 style=\"margin: 0; color: #23282d;\">This Week&lt;\/h3>\n                    &lt;p style=\"font-size: 24px; font-weight: bold; margin: 5px 0; color: #00a32a;\" id=\"week-visitors\">-&lt;\/p>\n                &lt;\/div>\n                &lt;div style=\"text-align: center;\">\n                    &lt;h3 style=\"margin: 0; color: #23282d;\">This Month&lt;\/h3>\n                    &lt;p style=\"font-size: 24px; font-weight: bold; margin: 5px 0; color: #d63638;\" id=\"month-visitors\">-&lt;\/p>\n                &lt;\/div>\n            &lt;\/div>\n            \n            &lt;canvas id=\"visitor-chart\" width=\"400\" height=\"200\" style=\"max-height: 200px;\">&lt;\/canvas>\n            &lt;div id=\"analytics-loading\" style=\"text-align: center; padding: 20px;\">Loading...&lt;\/div>\n        &lt;\/div>\n        \n        &lt;script>\n        jQuery(document).ready(function($) {\n            let chart = null;\n            \n            \/\/ Cleanup function to properly destroy chart\n            function destroyChart() {\n                if (chart &amp;&amp; typeof chart.destroy === 'function') {\n                    chart.destroy();\n                    chart = null;\n                }\n            }\n            \n            \/\/ Clean up when leaving the page\n            $(window).on('beforeunload', destroyChart);\n            \n            function loadAnalytics() {\n                const period = $('#analytics-period').val();\n                $('#analytics-loading').show();\n                \n                $.ajax({\n                    url: ajaxurl,\n                    type: 'POST',\n                    data: {\n                        action: 'get_visitor_data',\n                        period: period,\n                        nonce: '&lt;?php echo wp_create_nonce('visitor_analytics_nonce'); ?>'\n                    },\n                    success: function(response) {\n                        if (response.success) {\n                            updateSummary(response.data.summary);\n                            updateChart(response.data.chart_data);\n                        }\n                        $('#analytics-loading').hide();\n                    },\n                    error: function() {\n                        $('#analytics-loading').hide();\n                        alert('Error loading analytics data');\n                    }\n                });\n            }\n            \n            function updateSummary(summary) {\n                $('#today-visitors').text(summary.today || 0);\n                $('#week-visitors').text(summary.week || 0);\n                $('#month-visitors').text(summary.month || 0);\n            }\n            \n            function updateChart(chartData) {\n                const canvas = document.getElementById('visitor-chart');\n                const ctx = canvas.getContext('2d');\n                \n                \/\/ Properly destroy existing chart\n                if (chart) {\n                    chart.destroy();\n                    chart = null;\n                }\n                \n                \/\/ Clear the canvas\n                ctx.clearRect(0, 0, canvas.width, canvas.height);\n                \n                \/\/ Reset canvas size\n                canvas.style.width = '100%';\n                canvas.style.height = '200px';\n                \n                chart = new Chart(ctx, {\n                    type: 'line',\n                    data: {\n                        labels: chartData.labels,\n                        datasets: &#91;{\n                            label: 'Daily Visitors',\n                            data: chartData.data,\n                            borderColor: '#0073aa',\n                            backgroundColor: 'rgba(0, 115, 170, 0.1)',\n                            borderWidth: 2,\n                            fill: true,\n                            tension: 0.4\n                        }]\n                    },\n                    options: {\n                        responsive: true,\n                        maintainAspectRatio: false,\n                        interaction: {\n                            intersect: false,\n                            mode: 'index'\n                        },\n                        scales: {\n                            y: {\n                                beginAtZero: true,\n                                ticks: {\n                                    stepSize: 1,\n                                    precision: 0\n                                }\n                            },\n                            x: {\n                                grid: {\n                                    display: false\n                                }\n                            }\n                        },\n                        plugins: {\n                            legend: {\n                                display: false\n                            },\n                            tooltip: {\n                                backgroundColor: 'rgba(0, 0, 0, 0.8)',\n                                titleColor: '#fff',\n                                bodyColor: '#fff',\n                                cornerRadius: 4\n                            }\n                        },\n                        elements: {\n                            point: {\n                                radius: 3,\n                                hoverRadius: 6\n                            }\n                        }\n                    }\n                });\n            }\n            \n            \/\/ Event listeners\n            $('#analytics-period').change(loadAnalytics);\n            $('#refresh-analytics').click(loadAnalytics);\n            \n            \/\/ Initial load\n            loadAnalytics();\n        });\n        &lt;\/script>\n        &lt;?php\n    }\n    \n    \/**\n     * Enqueue admin scripts\n     *\/\n    public function enqueue_admin_scripts($hook) {\n        if ($hook === 'index.php') {\n            wp_enqueue_script('chart-js', 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/Chart.js\/3.9.1\/chart.min.js', array(), '3.9.1', true);\n        }\n    }\n    \n    \/**\n     * AJAX handler for getting visitor data\n     *\/\n    public function ajax_get_visitor_data() {\n        \/\/ Verify nonce\n        if (!wp_verify_nonce($_POST&#91;'nonce'], 'visitor_analytics_nonce')) {\n            wp_die('Security check failed');\n        }\n        \n        \/\/ Check user permissions\n        if (!current_user_can('manage_options')) {\n            wp_die('Insufficient permissions');\n        }\n        \n        global $wpdb;\n        $period = intval($_POST&#91;'period']);\n        \n        \/\/ Get summary data\n        $today = current_time('Y-m-d');\n        $week_ago = date('Y-m-d', strtotime('-7 days', current_time('timestamp')));\n        $month_ago = date('Y-m-d', strtotime('-30 days', current_time('timestamp')));\n        \n        $today_count = $wpdb->get_var($wpdb->prepare(\n            \"SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date = %s\", $today\n        ));\n        \n        $week_count = $wpdb->get_var($wpdb->prepare(\n            \"SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s\", $week_ago\n        ));\n        \n        $month_count = $wpdb->get_var($wpdb->prepare(\n            \"SELECT COUNT(*) FROM {$this->table_name} WHERE visit_date >= %s\", $month_ago\n        ));\n        \n        \/\/ Get chart data\n        $start_date = date('Y-m-d', strtotime(\"-{$period} days\", current_time('timestamp')));\n        \n        $chart_data = $wpdb->get_results($wpdb->prepare(\n            \"SELECT visit_date, COUNT(*) as visitor_count \n             FROM {$this->table_name} \n             WHERE visit_date >= %s \n             GROUP BY visit_date \n             ORDER BY visit_date ASC\",\n            $start_date\n        ));\n        \n        \/\/ Fill in missing dates with zero counts\n        $labels = array();\n        $data = array();\n        \n        for ($i = $period - 1; $i >= 0; $i--) {\n            $date = date('Y-m-d', strtotime(\"-{$i} days\", current_time('timestamp')));\n            $labels&#91;] = date('M j', strtotime($date));\n            \n            $count = 0;\n            foreach ($chart_data as $row) {\n                if ($row->visit_date === $date) {\n                    $count = intval($row->visitor_count);\n                    break;\n                }\n            }\n            $data&#91;] = $count;\n        }\n        \n        wp_send_json_success(array(\n            'summary' => array(\n                'today' => intval($today_count),\n                'week' => intval($week_count),\n                'month' => intval($month_count)\n            ),\n            'chart_data' => array(\n                'labels' => $labels,\n                'data' => $data\n            )\n        ));\n    }\n}\n\n\/\/ Initialize the plugin\nnew VisitorAnalytics();<\/code><\/pre>\n\n\n\n<p><\/p>\n<\/div>\n<\/details>\n\n\n\n<p>In gamedev news I want to make a &#8220;boss&#8221; area thing that the player has to destroy<\/p>\n\n\n\n<p>I think I want to incorporate that helix thingy as the &#8220;power core&#8221; (see: <a href=\"https:\/\/willkolb.com\/?p=627\">this post<\/a>). <\/p>\n\n\n\n<p>I&#8217;ve made a few iterations but they keep looking like the Red power ranger&#8217;s vape pen:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"562\" src=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-16-1024x562.png\" alt=\"\" class=\"wp-image-974\" srcset=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-16-1024x562.png 1024w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-16-300x165.png 300w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-16-768x422.png 768w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-16-500x274.png 500w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/image-16.png 1257w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"400\" height=\"650\" src=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/D3_citadel_030020.webp\" alt=\"\" class=\"wp-image-976\" style=\"width:409px;height:auto\" srcset=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/D3_citadel_030020.webp 400w, https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/D3_citadel_030020-185x300.webp 185w\" sizes=\"auto, (max-width: 400px) 100vw, 400px\" \/><figcaption class=\"wp-element-caption\">From <a href=\"https:\/\/half-life.fandom.com\/wiki\/Combine_Power_Generator\">https:\/\/half-life.fandom.com\/wiki\/Combine_Power_Generator<\/a><\/figcaption><\/figure>\n\n\n\n<p>I might go with some kind of oppressive robotic face instead? Like the Mussolini face from WWII.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"250\" height=\"228\" src=\"https:\/\/willkolb.com\/wp-content\/uploads\/2025\/05\/Palazzo_Braschi_Fascist_Poster_1934.png\" alt=\"\" class=\"wp-image-975\"\/><figcaption class=\"wp-element-caption\">from <a href=\"https:\/\/en.wikipedia.org\/wiki\/Palazzo_Braschi\">https:\/\/en.wikipedia.org\/wiki\/Palazzo_Braschi<\/a><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I added some visitor analytics to wordpress via a custom plugin As a non-web programmer this was surprisingly easy&#8230;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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":973,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,7,27],"tags":[],"class_list":["post-972","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blender","category-gamedev","category-softwaredev"],"_links":{"self":[{"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/posts\/972","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/willkolb.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=972"}],"version-history":[{"count":1,"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/posts\/972\/revisions"}],"predecessor-version":[{"id":978,"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/posts\/972\/revisions\/978"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/willkolb.com\/index.php?rest_route=\/wp\/v2\/media\/973"}],"wp:attachment":[{"href":"https:\/\/willkolb.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=972"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/willkolb.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=972"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/willkolb.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=972"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}