<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pan Poul</title>
    <description>The latest articles on DEV Community by Pan Poul (@panpoul).</description>
    <link>https://dev.to/panpoul</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4225749%2F13ec73bb-8bb6-4dd9-b632-884f8260ec2a.png</url>
      <title>DEV Community: Pan Poul</title>
      <link>https://dev.to/panpoul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/panpoul"/>
    <language>en</language>
    <item>
      <title>How I built a Fortnite Item Shop tracker with vanilla PHP (and why I didn't use a framework)</title>
      <dc:creator>Pan Poul</dc:creator>
      <pubDate>Wed, 04 Nov 2026 08:55:31 +0000</pubDate>
      <link>https://dev.to/panpoul/how-i-built-a-fortnite-item-shop-tracker-with-vanilla-php-and-why-i-didnt-use-a-framework-4ia8</link>
      <guid>https://dev.to/panpoul/how-i-built-a-fortnite-item-shop-tracker-with-vanilla-php-and-why-i-didnt-use-a-framework-4ia8</guid>
      <description>&lt;p&gt;I run a small web agency in Greece, and on the side I build niche tool sites. My latest one is &lt;strong&gt;&lt;a href="https://fortniteaccountvalue.com" rel="noopener noreferrer"&gt;Fortnite Account Value&lt;/a&gt;&lt;/strong&gt;: free calculators that show what a Fortnite locker would cost in the Item Shop, how rare each skin is, and what's in the shop today.&lt;/p&gt;

&lt;p&gt;It started as four calculators. This week it grew into a daily Item Shop tracker with full history for thousands of items, share images, three currencies and a Chrome extension. Here's how it's built and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack: boring on purpose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.1+, MySQL 8, vanilla JavaScript&lt;/li&gt;
&lt;li&gt;No framework, no build step, no npm on the server&lt;/li&gt;
&lt;li&gt;One front controller (&lt;code&gt;index.php&lt;/code&gt;), one folder per tool (&lt;code&gt;tools/&amp;lt;slug&amp;gt;/&lt;/code&gt;), a small admin panel and a web installer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why no framework? These sites run on cheap shared hosting, I build several of them on the same skeleton, and I want to upload a folder over FTP and be done. A 60-line router and a few static classes (&lt;code&gt;Database&lt;/code&gt;, &lt;code&gt;Settings&lt;/code&gt;, &lt;code&gt;Page&lt;/code&gt;) cover everything I need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data: one sync, every day
&lt;/h2&gt;

&lt;p&gt;Cosmetics come from &lt;a href="https://fortnite-api.com" rel="noopener noreferrer"&gt;Fortnite-API.com&lt;/a&gt;, an unofficial community API. A cron job hits a secret URL just after the shop resets at 00:00 UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://fortniteaccountvalue.com/cron/sync/?key=SECRET"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sync imports every cosmetic type (outfits, pickaxes, emotes, gliders…), then reads today's shop. Originally I only used the shop to update a "last seen" date and threw the line-up away. That turned out to be the most valuable data I had.&lt;/p&gt;

&lt;p&gt;Now every run stores the whole day: each offer, its section, its price, whether it's a bundle, and the items inside it. Each item also keeps its full list of shop dates. With that I can show things people actually search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Today's Item Shop&lt;/strong&gt;, with a "Back after 2.4 years" or "New" label on every item&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A page for every past day&lt;/strong&gt; (&lt;code&gt;/item-shop/2026-11-03/&lt;/code&gt;) and a calendar of all of them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A year × month history grid&lt;/strong&gt; on every item page, with its longest gap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Longest gone from the shop"&lt;/strong&gt; lists per item type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the history as a comma-separated &lt;code&gt;TEXT&lt;/code&gt; column instead of a separate table sounds lazy, but it means one row per item, and appending today's date is a single &lt;code&gt;UPDATE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;cosmetics&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;shop_dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;shop_dates&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;shop_dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;LOCATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shop_dates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shop_dates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;shop_dates&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>php</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
