diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..2cb7d2a --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8ed3589 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": ["unused-imports", "@typescript-eslint", "prettier"], + "rules": { + "prettier/prettier": "error", + "no-unused-vars": "off", + "unused-imports/no-unused-imports-ts": "error", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/ban-ts-comment": "off" + } +} diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml deleted file mode 100644 index a83df4a..0000000 --- a/.forgejo/workflows/deploy.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Deploy to Pages - -on: - push: - branches: ['main'] - pull_request: - branches: ['main'] - workflow_dispatch: - -concurrency: - group: 'pages' - cancel-in-progress: false - -jobs: - build: - runs-on: docker - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Lint - run: | - npm run lint - git diff - if [[ `git status --porcelain` ]]; then - exit 1 - fi - - - name: Typecheck - run: npm run typecheck - - - name: Build, Astro Audit & QA - run: | - npx playwright install chromium - npm run qa - - - name: Copy build to host pages mount - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: | - apt update && apt install -y rsync - mkdir -p /pages - rsync -a --delete dist/ /pages/schmelczer-dev - - - name: Copy build to staging pages mount - if: github.event_name == 'pull_request' - run: | - apt update && apt install -y rsync - mkdir -p /pages - rsync -a --delete dist/ /pages/schmelczer-dev-staging diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..14943fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/lint-and-deploy.yaml b/.github/workflows/lint-and-deploy.yaml new file mode 100644 index 0000000..6160131 --- /dev/null +++ b/.github/workflows/lint-and-deploy.yaml @@ -0,0 +1,35 @@ +name: Check, build and deploy to GitHub Pages +on: + push: + branches: + - main + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Lint + run: | + npm ci + npm run lint && git diff + if [[ `git status --porcelain` ]]; then + exit 1 + fi + build-and-deploy: + concurrency: ci-${{ github.ref }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install and Build + run: | + npm ci + npm run build + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4.4.1 + with: + branch: gh-pages + folder: dist diff --git a/.gitignore b/.gitignore index 38366ae..317395e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules dist -.astro +target .DS_Store diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 2bd5a0a..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/.prettierrc b/.prettierrc index 70d7dc0..afe8ac1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,13 +4,7 @@ "tabWidth": 2, "singleQuote": true, "endOfLine": "lf", - "plugins": ["prettier-plugin-astro"], - "overrides": [ - { - "files": "*.astro", - "options": { - "parser": "astro" - } - } - ] + "importOrder": ["^[./]", ".*", ".scss$"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true } diff --git a/.vscode/settings.json b/.vscode/settings.json index 80a5ba5..a08669b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,6 @@ "files.exclude": { "node_modules": true }, - "editor.rulers": [90], + "editor.rulers": [120], "editor.wordWrap": "on" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 37915af..49d45f9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "tasks": [ { - "label": "Lint", + "label": "Format and lint", "type": "shell", "command": "npm run lint", "group": "test", diff --git a/README.md b/README.md index 64893ae..f368051 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,24 @@ -# schmelczer.dev - -Engineering writeups by Andras Schmelczer: finished projects with the design constraints left in. Built with Astro, no required client JavaScript. - -Articles live in `src/content/posts`, project index entries in `src/content/projects`, and normal pages are rendered as static HTML. - -## Setup - -```sh -npm ci -npx playwright install --with-deps chromium # required before Playwright QA checks -``` - -## Commands - -```sh -npm run dev -npm run lint -npm run build -npm run preview -npm run qa -``` - -## Structure - -- `src/content/posts`: Markdown articles -- `src/content/projects`: project index entries -- `src/pages`: static routes -- `src/layouts`: page and post layouts -- `src/components`: reusable UI pieces -- `src/styles/global.css`: the visual system -- `public/media/downloads`: CV and thesis PDFs -- `public/media/video`: project videos +# Portfolio + +> An easy-to-configure timeline for your projects. + +[![Check, build and deploy to GitHub Pages](https://github.com/schmelczer/schmelczer.github.io/actions/workflows/lint-and-deploy.yaml/badge.svg)](https://github.com/schmelczer/schmelczer.github.io/actions/workflows/lint-and-deploy.yaml) + + +[Check out the live version.](https://schmelczer.dev) + +## Configuration + +- The actual content is in the [data](src/data) folder, starting with [portfolio.ts](src/data/portfolio.ts) +- The assets referenced should be located in [data/media](src/data/media) + +## Build + +1. `npm install` +2. `npm run build` +3. You can find the results in the [dist](dist) folder + +## Info + +- All images are converted to `WebP` after being imported into any file. + > Except for the og-image, and SVGs. \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs deleted file mode 100644 index 93a9357..0000000 --- a/astro.config.mjs +++ /dev/null @@ -1,148 +0,0 @@ -import { readdirSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import sitemap from '@astrojs/sitemap'; -import { defineConfig } from 'astro/config'; -import rehypeAutolinkHeadings from 'rehype-autolink-headings'; -import rehypeSlug from 'rehype-slug'; -import { visit } from 'unist-util-visit'; - -// Build a lookup of post slugs to their last modification dates so the sitemap -// can advertise accurate values to crawlers. astro:content isn't -// available inside the config, so we read post frontmatter directly. Our posts -// always use single-line scalar `date:` / `updated:` keys, so a small regex -// extraction is sufficient and intentional. -const postsDir = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - 'src/content/posts' -); - -function extractScalar(frontmatter, key) { - const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm')); - return match?.[1]?.replace(/^['"]|['"]$/g, ''); -} - -const postLastmodLookup = new Map( - readdirSync(postsDir, { withFileTypes: true }) - .filter((entry) => entry.isFile() && entry.name.endsWith('.md')) - .map((entry) => { - const raw = readFileSync(path.join(postsDir, entry.name), 'utf8'); - const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? ''; - const rawDate = - extractScalar(frontmatter, 'updated') ?? extractScalar(frontmatter, 'date'); - const parsed = rawDate ? new Date(rawDate) : null; - const valid = parsed && !Number.isNaN(parsed.valueOf()) ? parsed : null; - return [entry.name.replace(/\.md$/, ''), valid]; - }) - .filter(([, date]) => date !== null) -); - -export default defineConfig({ - site: 'https://schmelczer.dev', - trailingSlash: 'ignore', - integrations: [ - sitemap({ - filter: (page) => { - const path = new URL(page).pathname; - return !/^\/tags\/[^/]+\/?$/.test(path) && path !== '/404/'; - }, - serialize(item) { - const url = new URL(item.url); - const match = url.pathname.match(/^\/articles\/([^/]+)\/?$/); - let lastmod = item.lastmod; - if (match) { - const date = postLastmodLookup.get(match[1]); - if (date instanceof Date && !Number.isNaN(date.valueOf())) { - lastmod = date.toISOString(); - } - } - return { ...item, changefreq: 'monthly', ...(lastmod ? { lastmod } : {}) }; - }, - }), - ], - image: { - service: { entrypoint: 'astro/assets/services/sharp' }, - // SVG sources in src/content/**/_assets are author-controlled. - dangerouslyProcessSVG: true, - }, - vite: { - // Pre-bundle the analytics tracker during dev server startup. It is only - // referenced from a client ` diff --git a/src/components/ArticleList.astro b/src/components/ArticleList.astro deleted file mode 100644 index d2d3f9e..0000000 --- a/src/components/ArticleList.astro +++ /dev/null @@ -1,62 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import EntryThumbnail from './EntryThumbnail.astro'; -import TagList from './TagList.astro'; -import { ARTICLE_THUMBNAIL, articlePath, formatDate, formatDateShort } from '../lib/site'; - -interface Props { - posts: CollectionEntry<'posts'>[]; - showYear?: boolean; - tagLimit?: number; - timeline?: boolean; - // Opt-in: eagerly load thumbnails that are reliably above the fold. Lists - // below substantial content (related, about, 404) should leave this at zero. - eagerFirstThumbnail?: boolean; - eagerThumbnailCount?: number; -} - -const { - posts, - showYear = true, - tagLimit = 3, - timeline = false, - eagerFirstThumbnail = false, - eagerThumbnailCount = eagerFirstThumbnail ? 1 : 0, -} = Astro.props; ---- - -
    - { - posts.map((post, index) => { - const href = articlePath(post); - const eager = index < eagerThumbnailCount; - return ( -
  1. - - - -
  2. - ); - }) - } -
diff --git a/src/components/AtAGlance.astro b/src/components/AtAGlance.astro deleted file mode 100644 index c5417ea..0000000 --- a/src/components/AtAGlance.astro +++ /dev/null @@ -1,50 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import ProjectLinks from './ProjectLinks.astro'; - -type Link = CollectionEntry<'projects'>['data']['links'][number]; - -interface Props { - role?: string; - projectPeriod?: string; - stack?: string[]; - scale?: string; - outcome?: string; - links?: Link[]; - headingId: string; -} - -const { - role, - projectPeriod, - stack = [], - scale, - outcome, - links = [], - headingId, -} = Astro.props; - -const rows: Array<[string, string]> = []; -if (role) rows.push(['Role', role]); -if (projectPeriod) rows.push(['Period', projectPeriod]); -if (stack.length > 0) rows.push(['Stack', stack.join(', ')]); -if (scale) rows.push(['Scale', scale]); -if (outcome) rows.push(['Outcome', outcome]); ---- - -{ - rows.length > 0 && ( - - ) -} diff --git a/src/components/Breadcrumbs.astro b/src/components/Breadcrumbs.astro deleted file mode 100644 index ba039af..0000000 --- a/src/components/Breadcrumbs.astro +++ /dev/null @@ -1,33 +0,0 @@ ---- -interface Crumb { - href?: string; - label: string; -} - -interface Props { - items: Crumb[]; -} - -const { items } = Astro.props; -const lastIndex = items.length - 1; ---- - - diff --git a/src/components/EntryThumbnail.astro b/src/components/EntryThumbnail.astro deleted file mode 100644 index 750de48..0000000 --- a/src/components/EntryThumbnail.astro +++ /dev/null @@ -1,57 +0,0 @@ ---- -import type { ImageMetadata } from 'astro'; -import { Picture } from 'astro:assets'; - -interface Props { - src: ImageMetadata; - alt: string; - href?: string; - class?: string; - widths: number[]; - sizes: string; - loading?: 'lazy' | 'eager'; - fetchpriority?: 'high' | 'low' | 'auto'; - ariaLabel?: string; - // When the listing already has a focusable, screen-reader-visible title - // link, the thumbnail link is visually duplicative. We keep it clickable - // for pointer users but drop it from the tab order. The link still needs - // a name because some assistive tech exposes non-tabbable links. - decorative?: boolean; -} - -const { - src, - alt, - href, - class: extraClass, - widths, - sizes, - loading = 'lazy', - fetchpriority, - ariaLabel, - decorative = true, -} = Astro.props; - -const Tag = href ? 'a' : 'div'; -const isDecorativeLink = Boolean(href) && decorative; ---- - - - - diff --git a/src/components/Footer.astro b/src/components/Footer.astro deleted file mode 100644 index 8834892..0000000 --- a/src/components/Footer.astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -import { site } from '../lib/site'; - -const year = new Date().getFullYear(); ---- - - diff --git a/src/components/Header.astro b/src/components/Header.astro deleted file mode 100644 index 43dac2f..0000000 --- a/src/components/Header.astro +++ /dev/null @@ -1,158 +0,0 @@ ---- -import { navItems, site } from '../lib/site'; - -const currentPath = Astro.url.pathname; -const current = - currentPath === '/' || currentPath.endsWith('/') || /\.[^/]+$/.test(currentPath) - ? currentPath - : `${currentPath}/`; - -// Exact match for the current page; section match (descendant URLs) for -// ancestor links. `aria-current="page"` is reserved for the exact page, -// `"true"` indicates an ancestor section. -function currentState(href: string): 'page' | 'true' | undefined { - if (current === href) return 'page'; - if (href !== '/' && current.startsWith(href)) return 'true'; - return undefined; -} - -// Header shows nav items except Home and footer-only entries. RSS lives as a -// dedicated icon link to the right of the nav. -const headerNavItems = navItems.filter((item) => item.href !== '/' && !item.footerOnly); ---- - - - - - - - diff --git a/src/components/PostMedia.astro b/src/components/PostMedia.astro deleted file mode 100644 index 2a5f425..0000000 --- a/src/components/PostMedia.astro +++ /dev/null @@ -1,30 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import PostMediaFigure from './PostMediaFigure.astro'; - -type MediaItem = CollectionEntry<'posts'>['data']['media'][number]; - -interface Props { - items: MediaItem[]; -} - -const { items } = Astro.props; - -// Wrap in a gallery `