Aneurin Barker Snook 1 week ago
parent
commit
a2b7a6d0bf

+ 0 - 69
.github/workflows/deploy-github-pages.yml

@@ -1,69 +0,0 @@
-name: Deploy to GitHub Pages
-
-on:
-  workflow_dispatch:
-  push:
-    branches:
-      - develop
-
-jobs:
-  build:
-    name: Build
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Node.js
-        uses: actions/setup-node@v4
-        env:
-          NODE_AUTH_TOKEN: ${{ github.token }}
-        with:
-          node-version: 20.x
-
-      - name: Checkout repository
-        uses: actions/checkout@v4
-
-      - name: Set auth token
-        run: echo '//npm.pkg.github.com/:_authToken=${{ secrets.GH_PAT }}' >> .npmrc
-
-      - name: Install dependencies
-        run: npm ci
-
-      - name: Build statics
-        run: npm run build
-        env:
-          VITE_ANALYTICS_WEBSITE_ID: ${{ vars.ANALYTICS_WEBSITE_ID }}
-
-      - name: Upload artifact
-        uses: actions/upload-pages-artifact@v3
-        with:
-          name: github-pages
-          path: dist/
-          retention-days: 3
-
-  # https://github.com/actions/deploy-pages?tab=readme-ov-file#usage
-  deploy:
-    name: Deploy
-    runs-on: ubuntu-latest
-    needs: build
-    permissions:
-      pages: write
-      id-token: write
-    steps:
-      - name: Deploy to GitHub Pages
-        id: deployment
-        uses: actions/deploy-pages@v4
-
-  notify:
-    name: Send Discord workflow notification
-    runs-on: ubuntu-latest
-    needs: deploy
-    steps:
-      - name: Send notification
-        uses: annybs/action-notify-discord@v1
-        if: ${{ always() }}
-        with:
-          repository: ${{ github.repository }}
-          result: ${{ needs.deploy.result }}
-          run-id: ${{ github.run_id }}
-          run-number: ${{ github.run_number }}
-          webhook-url: ${{ secrets.DISCORD_WEBHOOK }}
-          workflow: ${{ github.workflow }}

+ 0 - 29
fonts/IosevkaCustom@30.3.0/README.md

@@ -1,29 +0,0 @@
-# Iosevka Custom build plan
-
-This build plan includes only glyphs `U+0000-036F` to reduce the filesize of font files (which are much larger than normal for the web, but I _really_ like it).
-
-Font files are stored as [public assets](../../public/assets/IosevkaCustom@30.3.0/) with a custom stylesheet for loading.
-
-## Usage
-
-To rebuild the font files for any reason, replicate the process:
-
-1. [Import/customise as necessary](https://typeof.net/Iosevka/customizer)
-2. [Make any further changes by hand in the generated TOML](https://github.com/be5invis/Iosevka/blob/main/doc/custom-build.md)
-3. Install the Iosevka repository and dependencies, using the instructions in the link above
-4. Execute `npm run build webfont-unhinted::IosevkaCustom`
-5. Execute `cp dist/IosevkaCustom/WOFF2-Unhinted/*.woff2 <project-root>/public/assets/IosevkaCustom@30.3.0`
-
-When excluding glyphs, [codepoints.net](https://codepoints.net/basic_multilingual_plane) is a helpful reference. [Convert the hexadecimal values to decimal](https://www.rapidtables.com/convert/number/hex-to-decimal.html) for use in TOML.
-
-> Note: the font directory is named for the [latest version at the time of writing](https://github.com/be5invis/Iosevka/releases/tag/v30.3.0) and should be updated as applicable if the font files are rebuilt in future.
-
-## Issues
-
-This solution is work in progress, mainly because font files are quite large. I have already taken some steps to reduce client-side impact:
-
-- Removed glyphs outside the main latin range, since I don't use them
-- Used unhinted font files, which causes console warnings (`downloadable font: maxp: Bad maxZones: 0`) but reduces file size noticeably
-- [Deferred loading font stylesheet](https://stackoverflow.com/a/65076370) to prioritise other resources
-
-There may be opportunity for further optimisation, but for now I'm happy especially since all other resources are so lightweight.

+ 0 - 37
fonts/IosevkaCustom@30.3.0/private-build-plans.toml

@@ -1,37 +0,0 @@
-[buildPlans.IosevkaCustom]
-family = "Iosevka Custom"
-spacing = "quasi-proportional"
-serifs = "sans"
-noCvSs = true
-exportGlyphNames = false
-noLigation = true
-
-[buildPlans.IosevkaCustom.excludeChars]
-ranges = [[880,65535]]
-
-[buildPlans.IosevkaCustom.weights.Regular]
-shape = 400
-menu = 400
-css = 400
-
-[buildPlans.IosevkaCustom.weights.Bold]
-shape = 700
-menu = 700
-css = 700
-
-[buildPlans.IosevkaCustom.widths.Normal]
-shape = 500
-menu = 5
-css = "normal"
-
-[buildPlans.IosevkaCustom.slopes.Upright]
-angle = 0
-shape = "upright"
-menu = "upright"
-css = "normal"
-
-[buildPlans.IosevkaCustom.slopes.Italic]
-angle = 9.4
-shape = "italic"
-menu = "italic"
-css = "italic"

+ 25 - 165
index.html

@@ -5,7 +5,6 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta name="description" content="I have been working with computers for over twenty years and love to solve technical and business problems for a fair price." />
     <title>Aneurin Barker Snook</title>
-    <link x-data="font" rel="preload" as="style" onload="this.onload=null; this.rel='stylesheet'" x-bind:href="url">
   </head>
   <body>
     <main x-data="external">
@@ -17,44 +16,13 @@
           </h1>
         </header>
 
-        <p><strong>Software engineer extraordinaire</strong></p>
+        <dl>
+          <dt>Occupation</dt>
+          <dd>Full Stack Software Developer</dd>
 
-        <p>I have been working with computers for over twenty years and love to solve technical and business problems for a fair price.</p>
-        <p>
-          I am a skilled and imaginative programmer with deep insight and excellent communication skills&#11834;and
-          a keen collaborator with colleagues in all aspects of business, including other engineers, project management, sales and marketing, and leadership.
-        </p>
-        <p>
-          <a href="#contact" data-umami-event="Jump to contact">Let me know what I can do for you,</a> or read on to learn more about me and what I might be able to offer.
-        </p>
-      </section>
-
-      <section id="services">
-        <header>
-          <h2>Services</h2>
-        </header>
-
-        <p>
-          I specialise in full-stack software development, and I also offer a vast breadth of skills
-          and knowledge in many aspects of business and operations.
-        </p>
-
-        <ul>
-          <li>Software architecture and implementation</li>
-          <li>Software integrations and API design</li>
-          <li>Infrastructure, methodology and documentation</li>
-          <li>Web UI/UX design and implementation</li>
-          <li>Code review, debugging, and profiling</li>
-          <li>Security audits</li>
-          <li>Legacy platform maintenance and migration</li>
-          <li>Requirements gathering, stakeholder engagement, and R&D</li>
-          <li>Technical leadership and mentoring</li>
-          <li>Consulting</li>
-        </ul>
-
-        <p>
-          I am always open to new challenges. <a href="#contact" data-umami-event="Jump to contact">Send me a message</a> to let me know what I can help you with.
-        </p>
+          <dt>Since</dt>
+          <dd>2009</dd>
+        </dl>
       </section>
 
       <section id="skills" x-data="skills">
@@ -62,103 +30,11 @@
           <h2>Skills</h2>
         </header>
 
-        <p>
-          Are you looking for expertise in a specific domain? This list provides an overview of pretty much everything I know.
-          If you have any questions, or something you need isn't on the list, <a href="#contact" data-umami-event="Jump to contact">get in touch</a> and I'd be happy to talk about it.
-        </p>
-
-        <div class="data-view">
-          <nav class="tags filter">
-            <header>
-              <h3>Filter</h3>
-            </header>
-            <template x-for="tag in tags" :key="tag">
-              <a x-bind:class="tagClass(tag)" x-on:click="setCurrentTag" x-text="tag" x-bind:href="`?skill=${tag}`" x-bind:data-tag="tag"></a>
-            </template>
-          </nav>
-
-          <ul class="data">
-            <template x-for="skill in visibleSkills">
-              <li>
-                <header>
-                  <h3>
-                    <span x-bind:title="`Tags: ${skill.tags.join(', ')}`" x-text="skill.name"></span>
-                  </h3>
-                </header>
-
-                <template x-if="descriptions">
-                  <p class="description" x-text="skill.description"></p>
-                </template>
-              </li>
-            </template>
-          </ul>
-        </div>
-
-        <template x-if="descriptions">
-          <p>
-            To see a bit less information, <a href="#" x-on:click="toggleDescriptions" href="">hide descriptions</a>.
-          </p>
-        </template>
-        <template x-if="!descriptions">
-          <p>
-            To see even more information, <a x-on:click="toggleDescriptions" x-bind:href="`?descriptions=1`">show descriptions</a>.
-          </p>
-        </template>
-      </section>
-
-      <section id="work">
-        <header>
-          <h2>Work</h2>
-        </header>
-
-        <p>
-          You are welcome to browse <a x-bind:href="codeUrl" target="_blank">my public code online</a>, including the <a x-bind:href="repoUrl" target="_blank" data-umami-event="Clicked link" data-umami-event-link="Website code">code of the website you're looking at right now</a>.
-        </p>
-
-        <p>
-          I also have an extensive history of professional work that I am happy to discuss in general terms.
-          Please <a href="#contact" data-umami-event="Jump to contact">send me a message</a> if you'd like to know more.
-        </p>
-      </section>
-
-      <section id="pricing">
-        <header>
-          <h2>Pricing</h2>
-        </header>
-
-        <p>
-          I am happy to negotiate a rate that works for you:
-        </p>
-
         <ul>
-          <li>Day rate or hourly rate</li>
-          <li>Fixed project cost</li>
-          <li>Out-of-hours retainer</li>
-          <li>Reduced rates for charitable organisations</li>
+          <template x-for="skill in skills">
+            <li x-text="skill.name"></li>
+          </template>
         </ul>
-
-        <p>
-          <a href="#contact" data-umami-event="Jump to contact">Get in touch</a> to discuss what you need from me.
-        </p>
-      </section>
-
-      <section id="availability">
-        <header>
-          <h2>Availability</h2>
-        </header>
-
-        <p>
-          I am currently offering <span class="cta">availability</span> to work.
-        </p>
-
-        <p>
-          I have a fully-equipped private office and favour remote working.
-          I am able to join captioned video calls or discuss your needs by email or instant messaging.
-        </p>
-
-        <p>
-          if you would like to see me in person, I am based in Nottingham (UK) and willing to travel, within reason, for meetings or hybrid working.
-        </p>
       </section>
 
       <section id="contact">
@@ -166,41 +42,25 @@
           <h2>Contact</h2>
         </header>
 
-        <p>
-          The best way to reach me is by email.
-          Send a message to <a x-bind:href="`mailto:${email}`" x-text="email" data-umami-event="Clicked link" data-umami-event-link="Email"></a> summarising what you're looking for&#11834;or just to say hello!
-          I look forward to hearing from you.
-        </p>
-
-        <p>
-          You can also find me on <a x-bind:href="githubUrl" target="_blank" data-umami-event="Clicked link" data-umami-event-link="GitHub">GitHub</a> and <a x-bind:href="linkedinUrl" target="_blank" data-umami-event="Clicked link" data-umami-event-link="LinkedIn">LinkedIn</a>.
-        </p>
+        <dl>
+          <dt>Email</dt>
+          <dd>
+            <a x-bind:href="`mailto:${email}`" x-text="email" data-umami-event="Clicked link" data-umami-event-link="Email"></a>
+          </dd>
+          
+          <dt>GitHub</dt>
+          <dd>
+            <a x-bind:href="githubUrl" target="_blank" data-umami-event="Clicked link" data-umami-event-link="GitHub" x-text="githubUrl"></a>
+          </dd>
+
+          <dt>LinkedIn</dt>
+          <dd>
+            <a x-bind:href="linkedinUrl" target="_blank" data-umami-event="Clicked link" data-umami-event-link="LinkedIn" x-text="linkedinUrl"></a>
+          </dd>
+        </dl>
       </section>
     </main>
 
-    <div x-data="color" id="color">
-      <button title="Change colour scheme" x-on:click="rotateColorScheme" data-umami-event="Change colour scheme">
-        <template x-if="currentScheme === 'auto'">
-          <!-- clock @ https://heroicons.com/mini -->
-          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
-            <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-13a.75.75 0 0 0-1.5 0v5c0 .414.336.75.75.75h4a.75.75 0 0 0 0-1.5h-3.25V5Z" clip-rule="evenodd" />
-          </svg>
-        </template>
-        <template x-if="currentScheme === 'light'">
-          <!-- sun @ https://heroicons.com/mini -->
-          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
-            <path d="M10 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2ZM10 15a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 15ZM10 7a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM15.657 5.404a.75.75 0 1 0-1.06-1.06l-1.061 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM6.464 14.596a.75.75 0 1 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM18 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 18 10ZM5 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 5 10ZM14.596 15.657a.75.75 0 0 0 1.06-1.06l-1.06-1.061a.75.75 0 1 0-1.06 1.06l1.06 1.06ZM5.404 6.464a.75.75 0 0 0 1.06-1.06l-1.06-1.06a.75.75 0 1 0-1.061 1.06l1.06 1.06Z" />
-          </svg>
-        </template>
-        <template x-if="currentScheme === 'dark'">
-          <!-- moon @ https://heroicons.com/mini -->
-          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
-            <path fill-rule="evenodd" d="M7.455 2.004a.75.75 0 0 1 .26.77 7 7 0 0 0 9.958 7.967.75.75 0 0 1 1.067.853A8.5 8.5 0 1 1 6.647 1.921a.75.75 0 0 1 .808.083Z" clip-rule="evenodd" />
-          </svg>
-        </template>
-      </button>
-    </div>
-
     <div x-data="analytics">
       <template x-if="scriptUrl && websiteId">
         <script defer x-bind:src="scriptUrl" x-bind:data-website-id="websiteId"></script>

BIN
public/assets/IosevkaCustom@30.3.0/IosevkaCustom-Bold.woff2


BIN
public/assets/IosevkaCustom@30.3.0/IosevkaCustom-BoldItalic.woff2


BIN
public/assets/IosevkaCustom@30.3.0/IosevkaCustom-Italic.woff2


BIN
public/assets/IosevkaCustom@30.3.0/IosevkaCustom-Regular.woff2


+ 0 - 37
public/assets/IosevkaCustom@30.3.0/style.css

@@ -1,37 +0,0 @@
-/* regular */
-@font-face {
-  font-family: 'Iosevka Custom';
-  font-weight: 400;
-  font-display: swap;
-  src: url(IosevkaCustom-Regular.woff2) format('woff2');
-  unicode-range: U+0000-0369;
-}
-
-/* bold */
-@font-face {
-  font-family: 'Iosevka Custom';
-  font-weight: 700;
-  font-display: swap;
-  src: url(IosevkaCustom-Bold.woff2) format('woff2');
-  unicode-range: U+0000-0369;
-}
-
-/* italic */
-@font-face {
-  font-family: 'Iosevka Custom';
-  font-style: italic;
-  font-weight: 400;
-  font-display: swap;
-  src: url(IosevkaCustom-Italic.woff2) format('woff2');
-  unicode-range: U+0000-0369;
-}
-
-/* bold italic */
-@font-face {
-  font-family: 'Iosevka Custom';
-  font-style: italic;
-  font-weight: 700;
-  font-display: swap;
-  src: url(IosevkaCustom-BoldItalic.woff2) format('woff2');
-  unicode-range: U+0000-0369;
-}

+ 29 - 349
public/data/skills.json

@@ -1,351 +1,31 @@
 [
-  {
-    "name": "Adobe Illustrator",
-    "tags": ["design"]
-  },
-  {
-    "name": "Adobe Photoshop",
-    "tags": ["design"],
-    "description": "I have used Photoshop all my career to create assets for the web, including UI, imagery, and other promotional content."
-  },
-  {
-    "name": "Affinity Designer",
-    "tags": ["design"],
-    "description": "I use Designer to work with vector-based imagery and create icons."
-  },
-  {
-    "name": "Affinity Publisher",
-    "tags": ["design"]
-  },
-  {
-    "name": "Agile",
-    "tags": ["knowledge"],
-    "description": "I am comfortable working in an agile environment."
-  },
-  {
-    "name": "Alpine.js",
-    "tags": ["js", "framework"],
-    "description": "A recent discovery that I find makes it very easy to build simple websites, like this one, without huge amounts of reactive boilerplate. I look forward to getting to know it better."
-  },
-  {
-    "name": "Apache",
-    "tags": ["framework"],
-    "description": "I have set up many websites using PHP or JS using an Apache front proxy. I prefer nginx these days but Apache is easier to use in some respects."
-  },
-  {
-    "name": "ArangoDB",
-    "tags": ["database"],
-    "description": "ArangoDB is a fantastic NoSQL graph database which offers a very accessible and powerful query language, AQL. I have used it for many large-scale Node server apps."
-  },
-  {
-    "name": "Backend development",
-    "tags": ["knowledge"],
-    "description": "I have extensive backend engineering experience from building numerous server apps and integrations throughout my career."
-  },
-  {
-    "name": "BDD",
-    "tags": ["knowledge"],
-    "description": "My experence working in a fast-paced retail environment taught me that goals are always shifting, and it's vital to be able to respond to evolving requirements as your business grows with your software."
-  },
-  {
-    "name": "Bitbucket",
-    "tags": ["operations", "platform"],
-    "description": "I have used Bitbucket for version control (git) and CI/CD via Pipelines. It integrates well with Jira, too."
-  },
-  {
-    "name": "Cassandra",
-    "tags": ["database"],
-    "description": "A clever solution for storing data at scale when you have a clear understanding of the queries you will use."
-  },
-  {
-    "name": "CI/CD",
-    "tags": ["operations", "knowledge"],
-    "description": "I try as much as possible to automate repetitive processes, including testing software, building and deploying it, creating backups etc. A well-organised CI/CD app can also help to visualise the reach of your operations."
-  },
-  {
-    "name": "Confluence",
-    "tags": ["platform"],
-    "description": "Confluence integrates well with Jira and Bitbucket, and does a satisfactory job as an information hub if carefully organised."
-  },
-  {
-    "name": "CSS/SASS",
-    "tags": ["design", "language"],
-    "description": "I write native and semantic CSS/SASS like poetry. I thrive in the intersection of style, content, and function."
-  },
-  {
-    "name": "Docker",
-    "tags": ["operations", "language", "platform"],
-    "description": "Containerization is a boon to any hardworking engineer that needs to run a bunch of different stuff without taking chances on their host OS. Docker works great in production, too."
-  },
-  {
-    "name": "Express",
-    "tags": ["js", "framework"],
-    "description": "Express provides a very developer-friendly web server that's easy to scale horizontally and extend."
-  },
-  {
-    "name": "Figma",
-    "tags": ["design"],
-    "description": "Figma is a great design tool that makes it trivial for designers to share assets I can immediately isolate and embed into the software I'm working on."
-  },
-  {
-    "name": "Frontend development",
-    "tags": ["knowledge"],
-    "description": "I am well-versed in past and present frontend and UX trends and try to keep up with all major frameworks, including React and Vue."
-  },
-  {
-    "name": "Full-stack development",
-    "tags": ["knowledge"],
-    "description": "I am adept at uniting frontend and backend development with accessible connectors and protocols."
-  },
-  {
-    "name": "git",
-    "tags": ["operations", "language"],
-    "description": "All development has been significantly easier ever since git joined my workflow. I work with greater confidence, knowing that I can always look to the past to guide the future."
-  },
-  {
-    "name": "GitHub",
-    "tags": ["operations", "platform"],
-    "description": "GitHub offers increasing value the more you use it, and its Actions are so easy to work with to enable CI/CD even for small projects. Always my first stop for hosted version control."
-  },
-  {
-    "name": "Go",
-    "tags": ["language"],
-    "description": "I adore Golang's simplicity - it makes networked apps quick to put together with minimal fuss, and easy to understand when you revisit them again in the future."
-  },
-  {
-    "name": "Google Analytics",
-    "tags": ["platform"],
-    "description": "I have provided basic and custom Google Analytics integrations for many clients, enabling them to better understand and react to customer behaviours."
-  },
-  {
-    "name": "Google Docs/Drive",
-    "tags": ["office"],
-    "description": "Google offer a very competitive office suite which fits 95% of my use cases perfectly. It makes it easy to share documentation and resources with my clients, and collaborative editing works exceptionally well."
-  },
-  {
-    "name": "Grafana",
-    "tags": ["operations", "platform"],
-    "description": "I use Prometheus and Grafana to aggregate metrics from different apps and create useful visualisations of their availability, behaviour, and performance."
-  },
-  {
-    "name": "GraphQL",
-    "tags": ["language", "knowledge"],
-    "description": "I have worked with GraphQL in Magento and Uniswap integrations. It's not my preference of API for technical and cost/benefit reasons but when it works, it's very clever."
-  },
-  {
-    "name": "gRPC",
-    "tags": ["framework"],
-    "description": "While I favour WebSocket or REST APIs in most cases, gRPC can be a good option for coupled connectivity in mature apps with a well-defined protocol."
-  },
-  {
-    "name": "HTML5/XHTML/XML",
-    "tags": ["language"],
-    "description": "HTML was my first web language, and it remains of critical importance for SEO and accessibility even in the era of web apps."
-  },
-  {
-    "name": "Ionic Framework",
-    "tags": ["js", "framework"],
-    "description": "I led a team of four developers building a Magento-based web app for a major retailer. Ionic was a strong choice to use off-the-shelf components and accelerate our wireframing."
-  },
-  {
-    "name": "JavaScript (ES6)",
-    "tags": ["js", "language"],
-    "description": "I am fluent in JavaScript, though I prefer to work in TypeScript wherever possible. It's a fine language for simple cases and offers much more power when augmented by type hints, especially in Intellisense."
-  },
-  {
-    "name": "Jenkins",
-    "tags": ["operations", "platform"],
-    "description": "I have managed Jenkins instances for clients which handle testing, building, and deploying apps and creating backups across their network."
-  },
-  {
-    "name": "Jira",
-    "tags": ["platform"],
-    "description": "Despite its complexity, Jira is a great tool for managing projects, particularly when its integrations are well-used."
-  },
-  {
-    "name": "Laravel",
-    "tags": ["php", "framework"],
-    "description": "This framework's clever use of PHP magic methods makes it elegant without becoming overly restrictive. Laravel has remained a competitive option while other frameworks have risen and fallen."
-  },
-  {
-    "name": "LevelDB",
-    "tags": ["database"],
-    "description": "LevelDB is a nice, simple key-value store that makes it easy to whip up a self-contained app without using an external storage service. Its speed makes up for certain limitations such as single read and write."
-  },
-  {
-    "name": "LibreOffice (OpenOffice)",
-    "tags": ["office"],
-    "description": "When Microsoft Office isn't available, or heaven forfend isn't working, LibreOffice fills the gap perfectly well and has a few tricks of its own. It's always good to have a backup plan."
-  },
-  {
-    "name": "Magento (Adobe Commerce)",
-    "tags": ["php", "js", "ecommerce", "framework", "platform"],
-    "description": "I have delivered beautiful and powerful Magento websites for boutique and major retailers, often developing complex integrations with other platforms. I have experience with both Open Source and Commerce editions and was lead developer on one of the first Magento websites to launch on Commerce Cloud."
-  },
-  {
-    "name": "MariaDB",
-    "tags": ["database"],
-    "description": "MariaDB is my go-to when I need a MySQL server for Magento or WordPress work. It's easy to set up, particularly in a Docker-enabled environment."
-  },
-  {
-    "name": "Markdown",
-    "tags": ["language"]
-  },
-  {
-    "name": "MetaMask",
-    "tags": ["web3", "framework"],
-    "description": "I have integrated with MetaMask to facilitate currency exchanges on the Ethereum network."
-  },
-  {
-    "name": "Microsoft Office",
-    "tags": ["office"],
-    "description": "Word, Excel, Powerpoint - we've all used them at some point, and sometimes even the more niche apps."
-  },
-  {
-    "name": "MongoDB",
-    "tags": ["js", "database"],
-    "description": "MongoDB works really well with Node.js apps, making the most of dynamic typing and offering a powerful declarative query API."
-  },
-  {
-    "name": "MySQL/SQL",
-    "tags": ["database"],
-    "description": "A popular choice for many apps in need of RDBMS. SQL can accomplish so much and so quickly when properly utilized."
-  },
-  {
-    "name": "New Relic",
-    "tags": ["operations", "platform"],
-    "description": "New Relic is a multifacted platform I haven't had full opportunity to explore, but it has been incredibly useful to capture and visualise logging across apps, making it easier to monitor and respond to issues particularly in a distributed network."
-  },
-  {
-    "name": "Next.js",
-    "tags": ["js", "framework"]
-  },
-  {
-    "name": "nginx",
-    "tags": ["framework"],
-    "description": "A very elegant and efficient HTTP server which I favour for most apps, particularly when combined with automatic SSL certificate issuance."
-  },
-  {
-    "name": "Node-RED",
-    "tags": ["operations", "language"],
-    "description": "I have used Node-RED to create integrations and functionality which may need to be accessible to non-technical users. The drag-and-drop interface works really well, and it's easy to create your own plugins if the extensive plugin library doesn't have everything you need."
-  },
-  {
-    "name": "Node.js",
-    "tags": ["js", "language"],
-    "description": "I am a Node aficionado and I try to make the most of the expansive standard library, but when deadlines loom, there's a million packages on npm to speed things up a bit. Node apps are one of the quickest ways to get off the ground, particularly when transpiling from TypeScript."
-  },
-  {
-    "name": "Notion",
-    "tags": ["office"],
-    "description": "I absolutely love Notion for both freeform and structured documentation. It's within easy reach for quick notetaking, yet offers enough power to organise large document databases. A must have for any dynamic organisation."
-  },
-  {
-    "name": "PayPal",
-    "tags": ["payments", "platform"],
-    "description": "I have integrated PayPal for ecommerce using their plugins, primarily in Magento. I have also patched issues in earlier versions of the plugin for clients who had issues with payment creation."
-  },
-  {
-    "name": "PHP",
-    "tags": ["php", "language"],
-    "description": "I have been with PHP since version 5.6 many years ago. While it hasn't completely shaken off its past, modern versions are much more approachable with better typing features and strong, structured OOP capabilities."
-  },
-  {
-    "name": "Prometheus",
-    "tags": ["operations", "platform"],
-    "description": "I use Prometheus and Grafana to aggregate metrics from different apps and create useful visualisations of their availability, behaviour, and performance."
-  },
-  {
-    "name": "React",
-    "tags": ["js", "framework"],
-    "description": "Functional React is a brilliant way to build complex web apps with interlocking functionality. Hooks make it easy to separate concerns by isolating internal logic from the UX."
-  },
-  {
-    "name": "REST API",
-    "tags": ["knowledge"],
-    "description": "Sometimes you just can't beat a straightforward HTTP request to get the data you need, and well-formed REST APIs offer familiar conventions that make full use of the HTTP protocol and receive attendant benefits."
-  },
-  {
-    "name": "SOAP",
-    "tags": ["framework"],
-    "description": "No longer the preeminent message format for integrated web services, but still useful to know, particularly when integrating particularly deep-set systems in industry and commerce."
-  },
-  {
-    "name": "Shell",
-    "tags": ["operations", "language"],
-    "description": "I use a terminal every day in the course of my work, whether on my own system or in a remote SSH session. My hands-on experience managing servers supports my ability to implement CI/CD."
-  },
-  {
-    "name": "Shopify",
-    "tags": ["platform"],
-    "description": "I believe Shopify is an excellent, low-complexity option for small businesses who need an online shop. I am familiar with managing shops and marketing integrations, and have some experience with their APIs and templating system."
-  },
-  {
-    "name": "Sketch",
-    "tags": ["design"]
-  },
-  {
-    "name": "Solidity",
-    "tags": ["web3", "language"],
-    "description": "I have reviewed and supported Solidity-based smart contracts for the Ethereum network."
-  },
-  {
-    "name": "Strangler pattern",
-    "tags": ["knowledge"],
-    "description": "Businesses are made up of many moving parts, and sometimes parts need to be replaced. Software has to be able to follow suit, be it through interfaces and abstraction or the use of microservices. I can help make this happen."
-  },
-  {
-    "name": "Stripe",
-    "tags": ["payments", "platform"],
-    "description": "I have integrated Stripe into client systems both using plugins and manually using their excellent SDKs and APIs. Stripe is comfortably one of the best payment processors available in terms of developer experience."
-  },
-  {
-    "name": "Tailwind CSS",
-    "tags": ["framework"],
-    "description": "As much as I like to roll out my own CSS tailored for purpose, it can get out of control in larger teams and busy projects with high code turnover, particularly when following BDD. Tailwind provides cohesion and compartmentalisation of styles without sacrificing flexibility."
-  },
-  {
-    "name": "TDD",
-    "tags": ["knowledge"],
-    "description": "Tests are crucial to the correctness of all software. I am adept at writing full-coverage unit tests and designing integration tests. Test-driven development is a good fit for businesses that have a particular need for robustness and a clear vision for their software implementation."
-  },
-  {
-    "name": "TypeScript",
-    "tags": ["js", "language"],
-    "description": "TypeScript is the perfect solution for code correctness in JavaScript apps. It has enabled me to build things of vastly greater scope and complexity without losing track of the flow of data."
-  },
-  {
-    "name": "Umami [Analytics]",
-    "tags": ["platform"],
-    "description": "Sometimes, often for privacy reasons, Google Analytics isn't the right choice for your organisation, but that doesn't mean you have to miss out on analytics. Umami is a strong, responsible, and open-source option for understanding your users' behaviour."
-  },
-  {
-    "name": "Vite",
-    "tags": ["js", "operations"],
-    "description": "Vite eliminates practically all of the complexity of frontend tooling and makes it incredibly easy to start, maintain, and build a web project. One of my favourite creations of recent years."
-  },
-  {
-    "name": "Vue.js",
-    "tags": ["js", "framework"],
-    "description": "I have built numerous frontend applications in Vue 2 and 3. It is particularly enjoyable to create Vue SFCs since the introduction of Composition API and improvements in TypeScript support."
-  },
-  {
-    "name": "WebSockets",
-    "tags": ["js", "framework", "language"],
-    "description": "WebSockets make it wonderfully simple to create real-time data streams between a server and its client, particularly in the browser."
-  },
-  {
-    "name": "web3.js",
-    "tags": ["js", "framework"]
-  },
-  {
-    "name": "WooCommerce",
-    "tags": ["php", "ecommerce", "framework", "platform"],
-    "description": "A solid option for small online shops that need a high degree of customisation without the cost of hosted solutions. I have built and maintained WooCommerce stores for several clients."
-  },
-  {
-    "name": "WordPress",
-    "tags": ["php", "framework", "platform"],
-    "description": "The granddaddy of CMS. Say what you will about coding in WordPress, but its versatility and huge variety of plugins has kept it relevant and viable for over two decades. I have extensive experience customising and integrating WordPress for a wide range of clients."
-  }
+  { "name": "Alpine.js" },
+  { "name": "Apache" },
+  { "name": "ArangoDB" },
+  { "name": "bash" },
+  { "name": "CSS" },
+  { "name": "Docker" },
+  { "name": "Express" },
+  { "name": "git" },
+  { "name": "Go" },
+  { "name": "GraphQL" },
+  { "name": "HTML5" },
+  { "name": "JavaScript" },
+  { "name": "LevelDB" },
+  { "name": "Magento" },
+  { "name": "MariaDB" },
+  { "name": "MongoDB" },
+  { "name": "MySQL" },
+  { "name": "nginx" },
+  { "name": "Node.js" },
+  { "name": "PHP" },
+  { "name": "Postgres" },
+  { "name": "React" },
+  { "name": "SQLite" },
+  { "name": "Tailwind" },
+  { "name": "TypeScript" },
+  { "name": "Vite" },
+  { "name": "Vue.js" },
+  { "name": "WebSockets" },
+  { "name": "WordPress" }
 ]

+ 0 - 37
src/components/Color.ts

@@ -1,37 +0,0 @@
-export type ColorScheme = 'auto' | 'dark' | 'light'
-
-export interface ColorState {
-  currentScheme: ColorScheme
-
-  rotateColorScheme(): void
-  setCurrentScheme(value: ColorScheme, save?: boolean): void
-}
-
-export default function Color() {
-  const state: ColorState = {
-    currentScheme: 'auto',
-
-    setCurrentScheme(value, save) {
-      this.currentScheme = value
-      document.body.classList.remove('color-auto', 'color-dark', 'color-light')
-      document.body.classList.add(`color-${value}`)
-
-      if (save) localStorage?.setItem('prefer-color-scheme', value)
-    },
-
-    rotateColorScheme() {
-      if (this.currentScheme === 'auto') this.setCurrentScheme('light', true)
-      else if (this.currentScheme === 'light') this.setCurrentScheme('dark', true)
-      else this.setCurrentScheme('auto', true)
-    },
-  }
-
-  function loadColorScheme() {
-    const prev = localStorage?.getItem('prefer-color-scheme')
-    if (prev) state.setCurrentScheme(prev as ColorScheme)
-  }
-
-  loadColorScheme()
-
-  return state
-}

+ 0 - 4
src/components/External.ts

@@ -1,12 +1,8 @@
 export default function External() {
   return {
-    businessUrl: 'https://www.forandwhile.com',
     githubUrl: 'https://github.com/annybs/',
     linkedinUrl: 'https://www.linkedin.com/in/aneurinbs/',
 
-    codeUrl: 'https://code.aneur.in/explore/repos',
-    repoUrl: 'https://code.aneur.in/aneurin/www.aneur.in',
-
     get email() {
       return 'ni.ruena@niruena'.split('').reverse().join('')
     },

+ 0 - 5
src/components/Font.ts

@@ -1,5 +0,0 @@
-export default function Font() {
-  return {
-    url: `//${document.location.host}/assets/IosevkaCustom@30.3.0/style.css`,
-  }
-}

+ 1 - 87
src/components/Skills.ts

@@ -1,114 +1,28 @@
 export interface Skill {
   name: string
-  link?: string
-  tags: string[]
-  detail?: string
 }
 
 export interface SkillsState {
-  currentTag: string | null
-  descriptions: boolean
   skills: Skill[]
-  tags: string[]
-  visibleSkills: Skill[]
 
   init(): void
-  setCurrentTag(e: MouseEvent): void
-  tagClass(value: string): string
-  toggleDescriptions(e: MouseEvent): void
 }
 
 export default function Skills(): SkillsState {
-  function readCurrentTag() {
-    const usp = new URLSearchParams(window.location.search)
-    return usp.get('skill')
-  }
-
-  function readCurrentDescriptions() {
-    const usp = new URLSearchParams(window.location.search)
-    return usp.get('descriptions') === '1'
-  }
-
-  function updateVisibleSkills(this: SkillsState) {
-    const tag = this.currentTag
-    if (tag) this.visibleSkills = this.skills.filter(skill => skill.tags.includes(tag))
-    else this.visibleSkills = this.skills
-  }
-
   return {
-    currentTag: null,
-    descriptions: false,
     skills: [],
-    visibleSkills: [],
-
-    get tags() {
-      return this.skills
-        .reduce((tags, skill) => {
-          for (const tag of skill.tags) {
-            if (tags.indexOf(tag) === -1) tags.push(tag)
-          }
-          return tags
-        }, <string[]>[])
-        .sort()
-    },
 
     async init() {
-      this.currentTag = readCurrentTag()
-      this.descriptions = readCurrentDescriptions()
-
       try {
         const res = await fetch(`//${document.location.host}/data/skills.json`)
         if (res.status !== 200) throw new Error('not OK')
 
         const data = await res.json() as Skill[]
         this.skills = (data).sort((a, b) => a.name.localeCompare(b.name))
-        updateVisibleSkills.apply(this)
       } catch (err) {
         const e = err as Error
         console.error(e.name, e.message)
       }
-    },
-
-    setCurrentTag(e) {
-      const tag = (e.target as HTMLAnchorElement)?.dataset?.tag
-      if (!tag) return
-
-      e.preventDefault()
-
-      const usp = new URLSearchParams(window.location.search)
-      if (tag === this.currentTag) {
-        this.currentTag = null
-        usp.delete('skill')
-      } else {
-        this.currentTag = tag
-        usp.set('skill', tag)
-      }
-
-      const search = usp.toString()
-      if (search) window.history.pushState({ tag }, '', `${window.location.pathname}?${search}`)
-      else window.history.pushState({ tag }, '', window.location.pathname)
-
-      updateVisibleSkills.apply(this)
-      return tag
-    },
-
-    tagClass(value: string) {
-      if (value === this.currentTag) return 'tag active'
-      return 'tag'
-    },
-
-    toggleDescriptions(e) {
-      e.preventDefault()
-
-      const usp = new URLSearchParams(window.location.search)
-      if (this.descriptions) usp.delete('descriptions')
-      else usp.set('descriptions', '1')
-
-      const search = usp.toString()
-      if (search) window.history.pushState(null, '', `${window.location.pathname}?${search}`)
-      else window.history.pushState(null, '', window.location.pathname)
-
-      this.descriptions = !this.descriptions
-    },
+    }
   }
 }

+ 0 - 4
src/main.ts

@@ -1,17 +1,13 @@
 import './style.scss'
 import Alpine from 'alpinejs'
 import Analytics from './components/Analytics'
-import Color from './components/Color'
 import External from './components/External'
-import Font from './components/Font'
 import Skills from './components/Skills'
 
 function main() {
   (window as unknown as { Alpine: typeof Alpine }).Alpine = Alpine
   Alpine.data('analytics', Analytics(import.meta.env.VITE_ANALYTICS_WEBSITE_ID || ''))
-  Alpine.data('color', Color)
   Alpine.data('external', External)
-  Alpine.data('font', Font)
   Alpine.data('skills', Skills)
   Alpine.start()
 }

+ 62 - 178
src/style.scss

@@ -1,32 +1,33 @@
-$bp-tablet: 600px;
 $bp-desktop: 900px;
-$bp-super: 1200px;
 
+$font-family: "Helvetica", "Arial", sans-serif;
 $font-weight: 400;
 $font-weight-bold: 700;
 
 $link-border-width: 0.2rem;
 
+$page-max-width: 600px;
+
 @mixin prefer-light {
   --bg-color: #e9e9e9;
-  --border-color: #ccc;
-  --cta-color: #e24350;
+  --border-color: #212121;
+  --cta-color: #554;
   --cta-fg-color: #e9e9e9;
-  --fg-color: #333333;
+  --fg-color: #212121;
   --tag-bg-color: #e9e9e9;
 }
 
 @mixin prefer-dark {
-  --bg-color: #333333;
-  --border-color: #999;
-  --cta-color: #e24350;
+  --bg-color: #212121;
+  --border-color: #e9e9e9;
+  --cta-color: #aa9;
   --cta-fg-color: #e9e9e9;
   --fg-color: #e9e9e9;
   --tag-bg-color: #e9e9e9;
 }
 
 :root {
-  font-family: 'Iosevka Custom', monospace;
+  font-family: $font-family;
   font-size: 12pt;
   font-weight: 400;
 
@@ -44,230 +45,113 @@ body {
 
   padding: 0;
   margin: 0;
-
-  .color-light {
-    @include prefer-light;
-  }
-
-  .color-dark {
-    @include prefer-dark;
-  }
 }
 
-@mixin button-icon($size: 2rem) {
-  background: none;
-  border: none;
-  color: var(--fg-color);
-  cursor: pointer;
-  display: block;
-  margin: 0;
-  padding: 0;
-
-  svg {
-    display: block;
-    fill: var(--fg-color);
-    height: 100%;
-    width: $size;
-  }
+@mixin bold {
+  font-weight: 700;
 }
 
-@mixin link($color: var(--cta-color)) {
-  border-bottom: $link-border-width solid $color;
-  color: $color;
-  text-decoration: none;
+@mixin italic {
+  font-style: italic;
 }
 
-@mixin link-hover {
+@mixin link($color: var(--cta-color), $bgColor: var(--bg-color)) {
+  color: $color;
   cursor: pointer;
+  text-decoration: none;
 
   &:hover {
-    border-bottom-color: var(--bg-color);
+    background-color: $color;
+    color: $bgColor;
   }
 }
 
 a {
   @include link;
-  @include link-hover;
 }
 
-h1 {
-  font-size: 2rem;
+h1, h2 {
   font-weight: $font-weight-bold;
-  line-height: 1.4em;
+  letter-spacing: -0.05rem;
+  line-height: 1em;
   margin: 0;
   padding: 0;
 }
 
+h1 {
+  font-size: 2rem;
+}
+
 h2 {
   font-size: 1.4rem;
-  font-weight: $font-weight-bold;
-  line-height: 1em;
-  margin: 0;
-  padding: 0;
 }
 
-h3 {
-  font-size: 1rem;
-  font-weight: $font-weight-bold;
-  line-height: 1em;
+dl {
   margin: 0;
-  padding: 0;
-}
-
-p {
-  line-height: 1.5em;
-  margin: 1rem 0;
 }
 
 em {
-  font-style: italic;
+  @include italic;
 }
 
 strong {
-  font-weight: 700;
-}
-
-ul {
-  display: flex;
-  flex-direction: column;
-  gap: 0.5rem;
-  list-style-position: inside;
-  list-style-type: square;
-  padding: 0;
-}
-
-li {
-  line-height: 1.5rem;
-}
-
-.cta {
-  color: var(--cta-color);
-  text-transform: uppercase;
+  @include bold;
 }
 
 main {
   display: flex;
   flex-direction: column;
-  gap: 1rem;
-  padding: 1rem;
+  gap: 2rem;
+  margin: 0 auto;
+  max-width: $page-max-width;
+  padding: 2rem 1rem;
 
-  @media (min-width: $bp-tablet) {
-    gap: 2rem;
-    padding: 2rem;
+  @media (min-width: $bp-desktop) {
+    padding: 4rem 2rem 2rem;
   }
 }
 
-#top {
-  header h1 {
+section {
+  header {
+    border-bottom: 0.2rem solid var(--border-color);
+    margin-bottom: 1rem;
+    padding-bottom: 0.5rem;
+  }
+
+  dl {
     display: flex;
     flex-direction: column;
+    gap: 0.5rem;
 
-    @media (min-width: $bp-desktop) {
-      display: initial;
+    dt {
+      @include bold;
     }
-  }
-}
 
-#skills {
-  .data-view {
-    .filter {
-      header {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        padding: 0.2rem 0.4rem;
-      }
+    dd {
+      margin: 0;
     }
+  }
 
-    .tags {
+  &#top {
+    header h1 {
       display: flex;
-      flex-direction: row;
-      flex-wrap: wrap;
-      gap: 0.5rem;
-    }
-
-    .tag {
-      border-radius: 0.3rem;
-      border-width: 0;
-      padding: 0.2rem 0.4rem;
+      flex-direction: column;
 
-      &.active {
-        background-color: var(--cta-color);
-        color: var(--cta-fg-color);
-      }
-    }
-
-    .data {
-      border-bottom: solid 0.2rem var(--border-color);
-      border-top: solid 0.2rem var(--border-color);
-      display: grid;
-      gap: 1.5rem;
-      grid-template-columns: 1fr;
-      height: min-content;
-      list-style: none;
-      padding: 2rem 0;
-      width: 100%;
-
-      @media screen and (min-width: $bp-tablet) {
-        grid-template-columns: 1fr 1fr;
-      }
-
-      @media screen and (min-width: $bp-desktop) {
-        grid-template-columns: 1fr 1fr 1fr;
-      }
-
-      @media screen and (min-width: $bp-super) {
-        grid-template-columns: 1fr 1fr 1fr 1fr;
-      }
-
-      .description {
-        font-size: 0.8rem;
-      }
-
-      .tags {
-        font-size: 0.8rem;
-        padding: 0.1rem 0.2rem;
+      @media (min-width: $page-max-width) {
+        display: initial;
       }
     }
+  }
 
-    @media (min-width: $bp-tablet) {
-      border-bottom: solid 0.2rem var(--border-color);
-      border-top: solid 0.2rem var(--border-color);
+  &#skills {
+    ul {
       display: flex;
       flex-direction: row;
-      gap: 2rem;
-
-      .filter {
-        flex-direction: column;
-        margin: 1rem 0;
-
-        header {
-          display: initial;
-          padding: 0;
-          margin-bottom: 0.5rem;
-        }
-      }
-
-      .data {
-        border-bottom-width: 0;
-        border-top-width: 0;
-        padding: 0 0 0 2rem;
-      }
+      flex-wrap: wrap;
+      gap: 0.5rem 0.8rem;
+      list-style: none;
+      margin: 0;
+      padding: 0;
     }
   }
 }
-
-#color {
-  position: fixed;
-  right: 1rem;
-  top: 1rem;
-
-  @media (min-width: $bp-tablet) {
-    right: 2rem;
-    top: 2rem;
-  }
-
-  button {
-    @include button-icon;
-  }
-}