Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { test, expect, devices, type ConsoleMessage, type Page } from '@playwright/test';
import { bio } from '../src/data/about';

// Stable, distinctive prefix of the first bio paragraph. Sourcing from the
// shared module keeps this assertion in sync with copy changes.
const BIO_OPENER = bio[0].slice(0, 40);

const IFRAME_APPS = [
{ id: 'dmarc-mx', name: 'dmarc.mx', url: 'https://dmarc.mx' },
Expand Down Expand Up @@ -67,7 +72,9 @@ test.describe('desktop golden path', () => {

// About auto-opens on first boot; clicking the icon focuses the singleton.
await page.locator('button[aria-label="Open About Cory"]').click();
await expect(page.locator('section[aria-label="About Cory window"]')).toBeVisible();
const aboutWindow = page.locator('section[aria-label="About Cory window"]');
await expect(aboutWindow).toBeVisible();
await expect(aboutWindow.getByText(BIO_OPENER)).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('2-about-open.png') });

// Brand-mark regression: AboutApp header renders the full mark as an <img>, not an emoji.
Expand Down Expand Up @@ -100,6 +107,16 @@ test.describe('desktop golden path', () => {
});
});

test.describe('about static page', () => {
test.use({ viewport: { width: 1440, height: 900 } });

test('renders shared bio on /about', async ({ page }) => {
await page.goto('/about', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { level: 1, name: 'Cory' })).toBeVisible();
await expect(page.getByText(BIO_OPENER)).toBeVisible();
});
});

// Strip defaultBrowserType — can't change browser at describe scope, only viewport/UA matter.
const { defaultBrowserType: _ignoredBrowser, ...iPhone14 } = devices['iPhone 14'];

Expand Down
31 changes: 12 additions & 19 deletions src/components/os/apps/AboutApp.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { bio, currentFocus, socialLinks, tagline } from '../../../data/about';

export default function AboutApp() {
return (
<div className="h-full overflow-y-auto bg-[var(--color-void)] px-7 py-6 text-[var(--color-text)]">
Expand All @@ -11,38 +13,29 @@ export default function AboutApp() {
<div>
<div className="font-mono text-[11px] uppercase tracking-[0.3em] text-[var(--color-amber)]">About</div>
<h1 className="mt-1 font-[var(--font-display)] text-2xl font-semibold tracking-tight">Cory</h1>
<p className="mt-1 text-sm text-[var(--color-muted)]">
Indie builder · Cloudflare-native · studio-of-one
</p>
<p className="mt-1 text-sm text-[var(--color-muted)]">{tagline}</p>
</div>
</header>

<section className="mt-6 space-y-3 text-sm leading-relaxed text-[var(--color-dim)]">
<p>
I make small, well-scoped software that does one job well and ships. Most of it lives on Cloudflare Workers and Pages, written in TypeScript, deployed continuously.
</p>
<p>
The constant threads: DNS and email security, RSS and feed discovery, contact utilities, AI / Claude Code tooling, and occasional generative-art experiments that exist mostly to make me smile.
</p>
<p>
<span className="font-mono text-[var(--color-amber)]">CortechOS</span> is my attempt at a single memorable home for all of it — every icon on this desktop opens one of the things I've built, running live.
</p>
{bio.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
</section>

<section className="mt-6">
<div className="font-mono text-[11px] uppercase tracking-[0.2em] text-[var(--color-muted)]">Current focus</div>
<ul className="mt-2 grid gap-2 text-xs">
<FocusRow badge="🛡️" title="dmarc.mx" note="DMARC, SPF, DKIM, BIMI, MTA-STS in one scan." />
<FocusRow badge="📣" title="donthype.me" note="RSS feed discovery and reading without the hype cycle." />
<FocusRow badge="📇" title="q-r.contact" note="Contact QR codes generated locally; no tracking." />
<FocusRow badge="🧠" title="Claude Code tooling" note="cclog, claude-view, claudzibit, karkinos — sharper agents." />
{currentFocus.map(({ badge, title, note }) => (
<FocusRow key={title} badge={badge} title={title} note={note} />
))}
</ul>
</section>

<section className="mt-6 grid gap-2 text-xs sm:grid-cols-3">
<LinkCard label="GitHub" href="https://github.com/schmug" value="github.com/schmug" />
<LinkCard label="LinkedIn" href="https://www.linkedin.com/in/cory-rankin/" value="linkedin.com/in/cory-rankin" />
<LinkCard label="Sponsor" href="https://github.com/sponsors/schmug" value="github.com/sponsors/schmug" />
{socialLinks.map(({ label, href, value }) => (
<LinkCard key={label} label={label} href={href} value={value} />
))}
</section>
</div>
);
Expand Down
33 changes: 33 additions & 0 deletions src/data/about.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { bio, currentFocus, socialLinks, tagline } from './about';

describe('about data', () => {
it('has a non-empty tagline', () => {
expect(tagline.trim().length).toBeGreaterThan(0);
});

it('has at least four non-empty bio paragraphs', () => {
expect(bio.length).toBeGreaterThanOrEqual(4);
for (const paragraph of bio) {
expect(paragraph.trim().length).toBeGreaterThan(0);
}
});

it('has at least one current-focus item with a badge, title, and note', () => {
expect(currentFocus.length).toBeGreaterThan(0);
for (const item of currentFocus) {
expect(item.badge.trim().length).toBeGreaterThan(0);
expect(item.title.trim().length).toBeGreaterThan(0);
expect(item.note.trim().length).toBeGreaterThan(0);
}
});

it('social links all use https and expose a visible value', () => {
expect(socialLinks.length).toBeGreaterThan(0);
for (const link of socialLinks) {
expect(link.href).toMatch(/^https:\/\//);
expect(link.label.trim().length).toBeGreaterThan(0);
expect(link.value.trim().length).toBeGreaterThan(0);
}
});
});
33 changes: 33 additions & 0 deletions src/data/about.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const tagline = 'Indie builder · Cloudflare-native · studio-of-one';

export const bio: readonly string[] = [
"I'm Cory. I build small, well-scoped software and ship it under cortech.online. Most of what I make is one person's worth of work: a DNS tool, an email-auth utility, a QR-code service, a stager for apartment photos. Some of it pays; most of it doesn't. That's fine — the point is that it exists.",
'"Studio of one" is the honest label. No co-founder, no roadmap meetings, no quarterly planning. I pick a problem I actually have, build the smallest thing that solves it, and put it online. If it\'s useful to someone else, even better.',
'The stack is Cloudflare — Pages, Workers, D1, KV, R2. Not because it\'s trendy, but because one person can run a real product on it without paging themselves at 3 AM. Edge-by-default removes a whole class of infra decisions I\'d rather not be making.',
'CortechOS, the desktop metaphor you\'re looking at, is a joke that kept going. A portfolio site should reflect the shape of the work. Mine is a pile of small windows that each do one thing. So: here\'s a pile of small windows.',
];

export type FocusItem = {
badge: string;
title: string;
note: string;
};

export const currentFocus: readonly FocusItem[] = [
{ badge: '🛡️', title: 'dmarc.mx', note: 'DMARC, SPF, DKIM, BIMI, MTA-STS in one scan.' },
{ badge: '📣', title: 'donthype.me', note: 'RSS feed discovery and reading without the hype cycle.' },
{ badge: '📇', title: 'q-r.contact', note: 'Contact QR codes generated locally; no tracking.' },
{ badge: '🧠', title: 'Claude Code tooling', note: 'cclog, claude-view, claudzibit, karkinos — sharper agents.' },
];

export type SocialLink = {
label: string;
href: string;
value: string;
};

export const socialLinks: readonly SocialLink[] = [
{ label: 'GitHub', href: 'https://github.com/schmug', value: 'github.com/schmug' },
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/cory-rankin/', value: 'linkedin.com/in/cory-rankin' },
{ label: 'Sponsor', href: 'https://github.com/sponsors/schmug', value: 'github.com/sponsors/schmug' },
];
43 changes: 25 additions & 18 deletions src/pages/about.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import Base from '../layouts/Base.astro';
import { bio, currentFocus, socialLinks, tagline } from '../data/about';
---

<Base title="About" description="Schmug — indie builder, small-tool maker, Cloudflare-native." canonical="https://cortech.online/about">
Expand All @@ -13,30 +14,36 @@ import Base from '../layouts/Base.astro';
<div>
<p class="font-mono text-[11px] uppercase tracking-[0.35em] text-[var(--color-amber)]">About</p>
<h1 class="mt-2 font-[var(--font-display)] text-3xl font-bold tracking-tight sm:text-4xl">Cory</h1>
<p class="mt-2 text-sm text-[var(--color-muted)]">{tagline}</p>
</div>
</div>

<div class="mt-6 max-w-none space-y-5 text-[var(--color-dim)]">
<p>
I'm a builder. I like making small, well-scoped things that do one job well and ship. My work lives mostly on Cloudflare (Workers, Pages) and tends to be TypeScript-first.
</p>
<p>
Current focus: DNS and email security (<a href="https://dmarc.mx" rel="noopener" class="text-[var(--color-amber)] hover:underline">dmarc.mx</a>), contact-card utilities (<a href="https://q-r.contact" rel="noopener" class="text-[var(--color-amber)] hover:underline">q-r.contact</a>), AI and Claude Code tooling (cclog, claude-view, claudzibit), and the occasional piece of generative art (AntArt, RepoGotchi).
</p>
<p>
This page is about to turn into <span class="font-mono text-[var(--color-amber)]">CortechOS</span> — a tiny branded desktop where every icon is one of those projects, running live in a window. Until then, here's the static version.
</p>
{bio.map((paragraph) => <p>{paragraph}</p>)}
</div>

<div class="mt-10 grid gap-3 text-sm sm:grid-cols-2">
<a href="https://github.com/schmug" rel="noopener" class="rounded-[var(--ct-radius)] border border-[var(--color-border)] bg-[var(--color-panel)]/60 p-4 transition hover:border-[var(--color-amber)]/60 hover:bg-[var(--color-panel-hi)]">
<div class="font-mono text-[11px] uppercase tracking-wider text-[var(--color-muted)]">GitHub</div>
<div class="mt-1 text-[var(--color-text)]">github.com/schmug</div>
</a>
<a href="https://github.com/sponsors/schmug" rel="noopener" class="rounded-[var(--ct-radius)] border border-[var(--color-border)] bg-[var(--color-panel)]/60 p-4 transition hover:border-[var(--color-amber)]/60 hover:bg-[var(--color-panel-hi)]">
<div class="font-mono text-[11px] uppercase tracking-wider text-[var(--color-muted)]">Sponsor</div>
<div class="mt-1 text-[var(--color-text)]">github.com/sponsors/schmug</div>
</a>
<div class="mt-10">
<div class="font-mono text-[11px] uppercase tracking-[0.2em] text-[var(--color-muted)]">Current focus</div>
<ul class="mt-3 grid gap-2 text-xs">
{currentFocus.map(({ badge, title, note }) => (
<li class="flex items-center gap-3 rounded-md border border-[var(--color-border)] bg-[var(--color-panel)]/60 px-3 py-2">
<span aria-hidden="true" class="text-base">{badge}</span>
<div class="min-w-0 flex-1">
<div class="font-mono text-[11px] text-[var(--color-text)]">{title}</div>
<div class="truncate text-[11px] text-[var(--color-muted)]">{note}</div>
</div>
</li>
))}
</ul>
</div>

<div class="mt-10 grid gap-3 text-sm sm:grid-cols-3">
{socialLinks.map(({ label, href, value }) => (
<a href={href} rel="noopener" class="group rounded-[var(--ct-radius)] border border-[var(--color-border)] bg-[var(--color-panel)]/60 p-4 transition hover:border-[var(--color-amber)]/60 hover:bg-[var(--color-panel-hi)]">
<div class="font-mono text-[11px] uppercase tracking-wider text-[var(--color-muted)]">{label}</div>
<div class="mt-1 text-[var(--color-text)] group-hover:text-[var(--color-amber)]">{value}</div>
</a>
))}
</div>

</section>
Expand Down