Parcourir la source

initial working page

Aneurin Barker Snook il y a 11 mois
Parent
commit
47caf08205
17 fichiers modifiés avec 1590 ajouts et 139 suppressions
  1. 11 0
      .editorconfig
  2. 1 0
      .npmrc
  3. 3 0
      eslint.config.js
  4. 117 3
      index.html
  5. 981 7
      package-lock.json
  6. 8 1
      package.json
  7. 14 0
      public/skills.json
  8. 0 1
      public/vite.svg
  9. 26 0
      src/components/Skills.ts
  10. 0 9
      src/counter.ts
  11. 5 0
      src/lib/request/index.ts
  12. 238 0
      src/lib/request/lib.ts
  13. 93 0
      src/lib/request/types.ts
  14. 9 22
      src/main.ts
  15. 0 96
      src/style.css
  16. 79 0
      src/style.scss
  17. 5 0
      src/types.ts

+ 11 - 0
.editorconfig

@@ -0,0 +1,11 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{js,json,ts,tsx,vue}]
+indent_style = space
+indent_size = 2

+ 1 - 0
.npmrc

@@ -0,0 +1 @@
+@annybs:registry=https://npm.pkg.github.com

+ 3 - 0
eslint.config.js

@@ -0,0 +1,3 @@
+const annybs = require('@annybs/eslint')
+
+module.exports = [...annybs]

+ 117 - 3
index.html

@@ -2,12 +2,126 @@
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Vite + TS</title>
+    <title>Aneurin Barker Snook</title>
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
   </head>
   <body>
-    <div id="app"></div>
+    <main>
+      <section id="top">
+        <header>
+          <h1>Aneurin Barker Snook</h1>
+        </header>
+
+        <p>
+          Software engineer extraordinaire.
+          Current availability: <span class="availability">limited</span>
+        </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">Send me a message</a> to let me know what I can help you with.
+        </p>
+      </section>
+
+      <section id="skills">
+        <header>
+          <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">get in touch</a> and I'd be happy to talk about it.
+        </p>
+
+        <ul x-data="skills">
+          <template x-for="skill in skills">
+            <li>
+              <span class="name" x-text="skill.name"></span>
+              <span class="description" x-text="skill.description"></span>
+              <nav class="tags">
+                <template x-for="tag in skill.tags">
+                  <a x-text="tag" x-bind:href="`?tag=${tag}#skills`"></a>
+                </template>
+              </nav>
+            </li>
+          </template>
+        </ul>
+      </section>
+
+      <section id="pricing">
+        <header>
+          <h2>Pricing</h2>
+        </header>
+
+        <p>
+          I offer two standard rates on a contract or ad-hoc basis:
+        </p>
+
+        <ul>
+          <li>£300 per day (7.5 hours) in increments of half a day</li>
+          <li>£45 per hour in increments of one hour</li>
+        </ul>
+
+        <p>
+          You can choose one of these according to your needs when booking me for work.
+          I may also be able to offer fixed pricing for a complete project, subject to a discovery phase to define your requirements.
+          This may be chargeable depending on the time needed. In some cases,
+          I can also offer an SLA on retainer to ensure my availability to you.
+        </p>
+
+        <p>
+          If you are a charitable organisation, I would love to work with you and can offer a reduced rate to suit your budgetary
+          requirements. <a href="#contact">Get in touch</a> to discuss what you need from me.
+        </p>
+      </section>
+
+      <section id="contact">
+        <header>
+          <h2>Contact</h2>
+        </header>
+
+        <p x-data="{ email: 'a@aneur.in' }">
+          The best way to reach me is by email.
+          Send a message to <a x-bind:href="`mailto:${email}`" x-text="email"></a> summarising what you're looking for—or just to say hello!
+        </p>
+
+        <p>
+          You can aso find me on these platforms:
+        </p>
+
+        <ul>
+          <li>GitHub <a href="https://github.com/annybs/" target="_blank">github.com/annybs</a>
+          <li>LinkedIn <a href="https://www.linkedin.com/in/aneurinbs/" target="_blank">linkedin.com/in/aneurinbs</a>
+        </ul>
+      </section>
+    </main>
     <script type="module" src="/src/main.ts"></script>
   </body>
 </html>

Fichier diff supprimé car celui-ci est trop grand
+ 981 - 7
package-lock.json


+ 8 - 1
package.json

@@ -2,15 +2,22 @@
   "name": "www.aneur.in",
   "private": true,
   "version": "0.0.0",
-  "type": "module",
   "scripts": {
     "dev": "vite",
     "build": "tsc && vite build",
+    "lint": "npx eslint .",
     "preview": "vite preview"
   },
   "devDependencies": {
+    "@types/alpinejs": "^3.13.10",
     "gh-pages": "^6.1.1",
     "typescript": "^5.2.2",
     "vite": "^5.2.0"
+  },
+  "dependencies": {
+    "@annybs/eslint": "^1.0.0",
+    "@annybs/request-js": "^1.0.2",
+    "alpinejs": "^3.14.0",
+    "sass": "^1.77.5"
   }
 }

+ 14 - 0
public/skills.json

@@ -0,0 +1,14 @@
+[
+  {
+    "name": "Adobe Commerce (Magento 2)",
+    "tags": ["framework", "php", "js", "ecommerce"]
+  },
+  {
+    "name": "Adobe Illustrator",
+    "tags": ["design"]
+  },
+  {
+    "name": "Adobe Photoshop",
+    "tags": ["design"]
+  }
+]

+ 0 - 1
public/vite.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 26 - 0
src/components/Skills.ts

@@ -0,0 +1,26 @@
+import { Skill } from '../types'
+import request, { ErrorResponse } from '@annybs/request-js'
+
+export default function Skills() {
+  interface State {
+    skills: Skill[]
+
+    init(): void
+  }
+
+  const state: State = {
+    skills: [],
+
+    async init() {
+      try {
+        const res = await request.get(`//${document.location.host}/skills.json`).send()
+        this.skills = res.json as Skill[]
+      } catch (err) {
+        const e = err as ErrorResponse
+        console.error(e.name, e.message)
+      }
+    },
+  }
+
+  return state
+}

+ 0 - 9
src/counter.ts

@@ -1,9 +0,0 @@
-export function setupCounter(element: HTMLButtonElement) {
-  let counter = 0
-  const setCounter = (count: number) => {
-    counter = count
-    element.innerHTML = `count is ${counter}`
-  }
-  element.addEventListener('click', () => setCounter(counter + 1))
-  setCounter(0)
-}

+ 5 - 0
src/lib/request/index.ts

@@ -0,0 +1,5 @@
+export * from './lib'
+export type * from './types'
+
+import * as request from './lib'
+export default request

+ 238 - 0
src/lib/request/lib.ts

@@ -0,0 +1,238 @@
+import type * as types from './types'
+
+function error(xhr: XMLHttpRequest, reason?: unknown): types.ErrorResponse {
+  console.log(xhr, reason)
+  let name = 'Error'
+  let message = xhr.statusText
+  let stack: string | undefined
+
+  if (reason instanceof Error) {
+    const err = reason as Error
+    name = err.name
+    message = err.message
+    stack = err.stack
+  }
+
+  return {
+    name,
+    message,
+    stack,
+    xhr,
+  }
+}
+
+export function request() {
+  interface Data {
+    body?: Blob | string
+    headers: Record<string, string>
+    method: types.Method
+    ok: (number | [number, number])[]
+    params?: URLSearchParams
+    timeout?: number
+    url: string
+  }
+
+  const data = <Data>{
+    headers: {},
+    ok: [[200, 299]],
+  }
+
+  const req: types.Request = {
+    bearer(value) {
+      req.header('authorization', `Bearer ${value}`)
+
+      return req
+    },
+
+    blob(value) {
+      data.body = value
+
+      return req
+    },
+
+    body(value: Blob | string | types.AnyObject) {
+      if (typeof value === 'string') {
+        data.body = value
+      } else if (value instanceof Blob) {
+        data.body = value
+      } else {
+        return req.json(value)
+      }
+
+      return req
+    },
+
+    header(name, value) {
+      data.headers[name] = value
+
+      return req
+    },
+
+    json(value) {
+      data.body = JSON.stringify(value)
+      req.header('content-type', 'application/json')
+
+      return req
+    },
+
+    method(value) {
+      data.method = value
+
+      return req
+    },
+
+    ok(value) {
+      data.ok.push(value)
+
+      return req
+    },
+
+    param(name, value) {
+      if (!data.params) data.params = new URLSearchParams()
+
+      data.params.append(name, value)
+
+      return req
+    },
+
+    params(value) {
+      if (!data.params) data.params = new URLSearchParams()
+
+      if (value instanceof URLSearchParams) {
+        for (const [name, val] of value.entries()) {
+          data.params.append(name, val)
+        }
+      } else {
+        for (const name in value) {
+          data.params.append(name, value[name])
+        }
+      }
+
+      return req
+    },
+
+    string(value) {
+      data.body = value
+
+      return req
+    },
+
+    timeout(value) {
+      data.timeout = value
+
+      return req
+    },
+
+    url(value) {
+      data.url = value
+
+      return req
+    },
+
+    send() {
+      return new Promise((resolve, reject) => {
+        const xhr = new XMLHttpRequest()
+
+        xhr.addEventListener('load', () => {
+          const statusOk = data.ok.reduce((prev, value) => {
+            if (!prev) return prev
+            if (typeof value === 'number') return xhr.status === value
+            return xhr.status >= value[0] && xhr.status <= value[1]
+          }, true)
+
+          if (statusOk) {
+            resolve(response(xhr))
+          } else {
+            reject(error(xhr))
+          }
+        })
+
+        xhr.addEventListener('error', (reason) => {
+          reject(error(xhr, reason))
+        })
+
+        if (data.timeout) xhr.timeout = data.timeout
+
+        let url = data.url
+        if (data.params) {
+          const query = data.params.toString()
+          if (url.match('?')) url += `&${query}`
+          else url += `?${query}`
+        }
+        xhr.open(data.method, url)
+
+        xhr.send(data.body)
+      })
+    },
+
+    then(resolve, reject) {
+      return this.send().then(resolve).catch(reject)
+    },
+  }
+
+  return req
+}
+
+function response(xhr: XMLHttpRequest) {
+  const res: types.Response = {
+    xhr,
+
+    get blob() {
+      if (xhr.responseType === 'blob') return xhr.response
+      throw new Error('responseType is not blob')
+    },
+
+    header(name) {
+      return xhr.getResponseHeader(name)
+    },
+
+    headers() {
+      return xhr.getAllResponseHeaders()
+        .split(/\r\n/)
+        .map((line) => {
+          const [name, value] = line.split(':')
+          return { name, value }
+        })
+        .reduce((hs, { name, value }) => {
+          hs[name] = value
+          return hs
+        }, <Record<string, string>>{})
+    },
+
+    get json() {
+      if (xhr.responseType === 'json') return xhr.response
+      if (xhr.responseType === 'text' || !xhr.responseType) return JSON.parse(xhr.responseText)
+      throw new Error('responseType is not json or text')
+    },
+
+    get string() {
+      return xhr.responseText
+    },
+  }
+
+  return res
+}
+
+export function del(url: string) {
+  return request().method('DELETE').url(url)
+}
+
+export function get(url: string) {
+  return request().method('GET').url(url)
+}
+
+export function options(url: string) {
+  return request().method('OPTIONS').url(url)
+}
+
+export function patch(url: string) {
+  return request().method('PATCH').url(url)
+}
+
+export function post(url: string) {
+  return request().method('POST').url(url)
+}
+
+export function put(url: string) {
+  return request().method('PUT').url(url)
+}

+ 93 - 0
src/lib/request/types.ts

@@ -0,0 +1,93 @@
+export type AnyObject = Array<unknown> | Record<string | number | symbol, unknown>
+
+export interface ErrorResponse extends Error {
+  xhr: XMLHttpRequest
+}
+
+/** HTTP request method. */
+export type Method = 'DELETE' | 'GET' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT'
+
+/** Request data. */
+export interface Request extends PromiseLike<Response> {
+  /** Set the bearer token for the `authorization` header. */
+  bearer(value: string): Request
+
+  /** Unambiguously set a Blob as the request body. */
+  blob(value: Blob): Request
+
+  /** Set a Blob as the request body. */
+  body(value: Blob): Request
+  /** Set a string as the request body. */
+  body(value: string): Request
+  /**
+   * Encode an object as the request body.
+   * This will automatically set a `content-type: application/json` header.
+   */
+  body<T extends AnyObject>(value: T): Request
+
+  /** Set a request header. */
+  header(name: string, value: string): Request
+
+  /**
+   * Unambiguously encode an object as the request body.
+   * This will automatically set a `content-type: application/json` header.
+   */
+  json<T extends AnyObject>(value: T): Request
+
+  /** Set the request method. */
+  method(value: Method): Request
+
+  /**
+   * Add an "OK" response status code, preventing a client error being thrown if that status code is received.
+   * Status codes in the `2xx` range are always OK.
+   */
+  ok(value: number): Request
+  /**
+   * Add a range of "OK" response status codes, preventing a client error being thrown if a status code is received within that range.
+   * Status codes in the `2xx` range are always OK.
+   */
+  ok(start: number, end: number): Request
+
+  /** Set a request URL parameter. */
+  param(name: string, value: string): Request
+
+  /** Set request URL parameters. */
+  params(data: Record<string, string>): Request
+  /** Set request URL parameters. */
+  params(data: URLSearchParams): Request
+
+  /** Unambiguously set a string as the request body. */
+  string(value: string): Request
+
+  /** Set the request timeout (milliseconds). */
+  timeout(value: number): Request
+
+  /** Set the request URL. */
+  url(value: string): Request
+
+  /** Send the HTTP request via XMLHttpRequest. */
+  send(): Promise<Response>
+}
+
+export interface Response {
+  /** XMLHttpRequest object. */
+  xhr: XMLHttpRequest
+
+  /** Retrieve the response body as a Blob. */
+  blob: Blob
+
+  /** Get a response header. */
+  header(name: string): string | null
+
+  /** Get all response headers. */
+  headers(): Record<string, string>
+
+  /**
+   * Parse the response body from JSON.
+   * This can be cast to any type using the `as` keyword.
+   */
+  json: unknown
+
+  /** Retrieve the response body as a string. */
+  string: string
+}

+ 9 - 22
src/main.ts

@@ -1,24 +1,11 @@
-import './style.css'
-import typescriptLogo from './typescript.svg'
-import viteLogo from '/vite.svg'
-import { setupCounter } from './counter.ts'
+import './style.scss'
+import Alpine from 'alpinejs'
+import Skills from './components/Skills'
 
-document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
-  <div>
-    <a href="https://vitejs.dev" target="_blank">
-      <img src="${viteLogo}" class="logo" alt="Vite logo" />
-    </a>
-    <a href="https://www.typescriptlang.org/" target="_blank">
-      <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
-    </a>
-    <h1>Vite + TypeScript</h1>
-    <div class="card">
-      <button id="counter" type="button"></button>
-    </div>
-    <p class="read-the-docs">
-      Click on the Vite and TypeScript logos to learn more
-    </p>
-  </div>
-`
+function main() {
+  (window as unknown as { Alpine: typeof Alpine }).Alpine = Alpine
+  Alpine.data('skills', Skills)
+  Alpine.start()
+}
 
-setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
+main()

+ 0 - 96
src/style.css

@@ -1,96 +0,0 @@
-:root {
-  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
-  line-height: 1.5;
-  font-weight: 400;
-
-  color-scheme: light dark;
-  color: rgba(255, 255, 255, 0.87);
-  background-color: #242424;
-
-  font-synthesis: none;
-  text-rendering: optimizeLegibility;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-a {
-  font-weight: 500;
-  color: #646cff;
-  text-decoration: inherit;
-}
-a:hover {
-  color: #535bf2;
-}
-
-body {
-  margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
-}
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
-
-#app {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-  text-align: center;
-}
-
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-  transition: filter 300ms;
-}
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.vanilla:hover {
-  filter: drop-shadow(0 0 2em #3178c6aa);
-}
-
-.card {
-  padding: 2em;
-}
-
-.read-the-docs {
-  color: #888;
-}
-
-button {
-  border-radius: 8px;
-  border: 1px solid transparent;
-  padding: 0.6em 1.2em;
-  font-size: 1em;
-  font-weight: 500;
-  font-family: inherit;
-  background-color: #1a1a1a;
-  cursor: pointer;
-  transition: border-color 0.25s;
-}
-button:hover {
-  border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
-  outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
-  :root {
-    color: #213547;
-    background-color: #ffffff;
-  }
-  a:hover {
-    color: #747bff;
-  }
-  button {
-    background-color: #f9f9f9;
-  }
-}

+ 79 - 0
src/style.scss

@@ -0,0 +1,79 @@
+:root {
+  font-family: 'Roboto Mono', 'Courier New', Courier, monospace;
+  font-size: 12pt;
+
+  --bg-color: #e9e9e9;
+  --cta-color: #e24350;
+  --fg-color: #333333;
+}
+
+html, body {
+  background-color: var(--bg-color);
+  color: var(--fg-color);
+
+  padding: 0;
+  margin: 0;
+}
+
+@mixin link($color: var(--cta-color)) {
+  border-bottom: 0.2em solid $color;
+  color: $color;
+  text-decoration: none;
+}
+
+@mixin link-hover {
+  cursor: pointer;
+
+  &:hover {
+    border-bottom-color: var(--bg-color);
+  }
+}
+
+a {
+  @include link;
+  @include link-hover;
+}
+
+h1 {
+  font-size: 2rem;
+  font-weight: 700;
+  line-height: 1em;
+}
+
+h2 {
+  font-size: 1.4rem;
+  font-weight: 700;
+  line-height: 1em;
+}
+
+p {
+  line-height: 1.5em;
+  margin: 1rem 0;
+}
+
+ul {
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+  list-style-position: inside;
+  list-style-type: square;
+  padding: 0;
+}
+
+li {
+  line-height: 1.5rem;
+}
+
+main {
+  display: flex;
+  flex-direction: column;
+  gap: 2rem;
+  margin: 2rem;
+}
+
+#top {
+  .availability {
+    @include link;
+    text-transform: uppercase;
+  }
+}

+ 5 - 0
src/types.ts

@@ -0,0 +1,5 @@
+export interface Skill {
+  name: string
+  tags: string[]
+  detail?: string
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff