From 52d7814ee6321d54bd80ada535f2143bdd7ad33a Mon Sep 17 00:00:00 2001 From: Tomasz Stanisz Date: Wed, 7 Oct 2026 16:39:55 +0200 Subject: [PATCH] Show Car Brain as an independent product --- AGENTS.md | 2 +- README.md | 6 +- e2e/site.spec.ts | 6 +- src/content/carBrain.ts | 85 +++++++++++++++++++++------- src/sections/Now/CarBrainShots.tsx | 13 ++++- src/sections/Now/NowSection.test.tsx | 22 ++++++- src/sections/Now/NowSection.tsx | 82 +++++++++++++++------------ src/sections/Now/now.css | 29 +++++++++- 8 files changed, 172 insertions(+), 73 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7abfe18..759cad4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ All of these must pass before a change is done. CI (`.github/actions/check/`) ru - **Section links.** In-page section links go through `scrollToSection`, which moves focus to the section's heading. Off the home page they point to `/#id`. - **Per-page metadata.** Every page sets it with `pageMetadata()` from `src/lib/metadata.ts`. New public pages also belong in `src/app/sitemap.ts` (the Playwright suite checks the sitemap matches `PAGES`). - **Dark bands.** Inside `.band` (header row, hero, approach, Now building card) the generic tokens already hold band values. Use `--text`, `--surface`, `--accent` and so on; don't hard-code band colours, or the Terminal skin can't reset them. -- **Car Brain gate.** `CAR_BRAIN_PUBLISHED` in `src/content/carBrainLinks.ts` stays `false` until Tomasz says the App Store listing is live. Keep the store links behind it, and never import `src/content/carBrain.ts` into client code, or the links reach the build while unpublished. +- **Car Brain gate.** `CAR_BRAIN_PUBLISHED` in `src/content/carBrainLinks.ts` stays `false` until Tomasz says the App Store listing is live. The independent-product card is visible during development; keep the store links behind the gate, and never import `src/content/carBrain.ts` into client code, or the links reach the build while unpublished. - **Accessibility.** Keyboard access, visible focus, reduced motion, and no sideways scroll at 320px are all required. The axe scans must stay at zero violations. ## Content diff --git a/README.md b/README.md index a2e82cb..24f8b9a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ GitHub Actions runs all of these on Node 24.21.0, through the shared steps in `. - `src/mdx-components.tsx` — how MDX pages render: one article in the prose style (`src/components/Prose/prose.css`), an id on every `h2`, tables that scroll on their own, external links in a new tab, and the case-study components - `src/lib/metadata.ts` — title, description, canonical and Open Graph tags for each page - `src/content/publicProfile.ts` — the public wording and links: hero, architecture panel, work cards, the Now building card, approach, timeline and contact -- `src/content/carBrain.ts` and `src/content/carBrainLinks.ts` — the Car Brain card and its publication gate. While `CAR_BRAIN_PUBLISHED` is `false`, the card is not rendered and the build contains no store link. The links sit behind the gate, so the build drops them; client code imports only `carBrainLinks.ts`, never the card content. +- `src/content/carBrain.ts` and `src/content/carBrainLinks.ts` — the Car Brain card and its publication gate. The independent-product card is visible during development in EN/PL. While `CAR_BRAIN_PUBLISHED` is `false`, the build contains no store link. The links sit behind the gate, so the build drops them; client code imports only `carBrainLinks.ts`, never the card content. - `src/components/NavBar/` — section navigation ("← All work" on case studies), mobile menu, and the theme and style controls - `src/components/Hero/` and `src/components/ArchitecturePanel/` — the hero band and its clickable CMS diagram - Client components are kept small: NavBar with the command palette, `ArchitecturePanel`, `src/sections/Now/CarBrainShots.tsx`, `src/components/RevealOnView/` (the E2E bars' entrance) and `CaseStudyToc`. Everything else is a Server Component. @@ -102,7 +102,7 @@ Releases are tagged on `main` and listed on the repository's Releases page. `pac The Vitest suite checks focus after Escape and after choosing a section, hash updates, Back, reduced motion, closed-menu removal from keyboard navigation, desktop links after a resize, the saved theme and skin, the hero's section link, and links off the home page. The Playwright suite covers the built site in a real browser. Neither replaces a manual browser pass. -Look at 1440, 768, 390, and 320px, and at both sides of the 801px navigation switch. Check both skins and both themes, zoom, the long email, the architecture panel and the case-study "On this page" list, and that the page does not scroll sideways. Flip `CAR_BRAIN_PUBLISHED` locally to check the Car Brain card too. +Look at 1440, 768, 390, and 320px, and at both sides of the 801px navigation switch. Check both skins and both themes, zoom, the long email, the architecture panel and the case-study "On this page" list, and that the page does not scroll sideways. Check the Car Brain product card and screenshot controls without changing `CAR_BRAIN_PUBLISHED`; store availability remains a separate gate. On 2026-09-29, `https://3d-portfolio.devthomas.pl/` did not resolve (DNS `NXDOMAIN`). The footer does not link to it. GitHub, this portfolio, and the CV remain linked. @@ -135,7 +135,7 @@ env -u BLOG_PREVIEW_DIR npm run build npm run test:e2e ``` -Article headings must have unique, non-empty generated IDs. Polish characters are normalized for heading anchors. Use `ClientArchitecture` only for the Car Brain article, with `locale="en"` or `locale="pl"`; it distinguishes implemented authentication from planned web data paths. Publication does not enable the independent Car Brain launch card or App Store links. +Article headings must have unique, non-empty generated IDs. Polish characters are normalized for heading anchors. Use `ClientArchitecture` only for the Car Brain article, with `locale="en"` or `locale="pl"`; it distinguishes implemented authentication from planned web data paths. Article publication does not enable App Store links; the independent product card describes ongoing development. ## English / Polish localization diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts index a092617..965a920 100644 --- a/e2e/site.spec.ts +++ b/e2e/site.spec.ts @@ -464,7 +464,7 @@ test("the E2E bars grow in once they scroll into view", async ({ page }) => { await expect(bars).toHaveAttribute("data-reveal", "revealed"); }); -test("Car Brain and its store link appear only once it is published", async ({ +test("Car Brain is visible while its store link remains gated", async ({ page, }) => { for (const path of ["/", "/look/standard-light/"]) { @@ -472,8 +472,8 @@ test("Car Brain and its store link appear only once it is published", async ({ const html = await page.content(); expect(html.includes("apps.apple.com")).toBe(carBrain.published); await expect( - page.getByRole("heading", { level: 3, name: carBrain.title }) - ).toHaveCount(carBrain.published ? 1 : 0); + page.getByRole("heading", { level: 3, name: carBrain.title, exact: true }) + ).toHaveCount(1); } }); diff --git a/src/content/carBrain.ts b/src/content/carBrain.ts index ec152d5..8a75981 100644 --- a/src/content/carBrain.ts +++ b/src/content/carBrain.ts @@ -1,8 +1,6 @@ import { CAR_BRAIN_PUBLISHED, carBrainLinks } from "./carBrainLinks"; -// Car Brain, the app I built on my own. While it is unpublished (see -// ./carBrainLinks.ts), the card is not rendered and no store link appears -// anywhere: not in the page, the structured data or the command palette. +// The product card is visible during development; store links remain gated. export type CarBrainShot = { id: string; @@ -14,10 +12,17 @@ export type CarBrainShot = { export type CarBrain = { published: boolean; eyebrow: string; - years: string; title: string; text: string; tags: string[]; + role: string; + architecture: string; + features: string; + automation: string; + status: string; + articleLabel: string; + screensLabel: string; + sampleNote: string; appStoreLabel: string; appStoreUrl: string; siteLabel: string; @@ -30,17 +35,21 @@ export type CarBrain = { export const carBrain: CarBrain = { published: CAR_BRAIN_PUBLISHED, - eyebrow: "Shipped on my own", - years: "2024–2025", - title: "Car Brain: a vehicle-management app, built end to end", - text: "Fuel, maintenance and expenses for one or many cars, with an analytics dashboard, service reminders, AI-powered insights and a night-driving theme. I built all of it: the React Native app for iOS and Android, the Appwrite backend and cloud functions, and the landing page.", - tags: [ - "React Native", - "iOS", - "Android", - "Appwrite", - "Google & Apple sign-in", - ], + eyebrow: "Independent product", + title: "Car Brain", + role: "Creator & Developer", + text: "A vehicle-management app I’m building for iOS, Android and the web around a shared Appwrite backend.", + architecture: + "React Native powers the mobile app and Next.js the web client under development. Both use TypeScript. Cloud functions handle AI insights and VIN decoding.", + features: + "Vehicle management · Maintenance, fuel & costs · Reports & analytics", + automation: + "AI-powered vehicle insights · VIN decoding with automatic form filling", + status: "Mobile app reactivation and web development in progress.", + articleLabel: "Why I built Car Brain", + screensLabel: "Car Brain screens", + sampleNote: "App screens with sample data.", + tags: ["React Native", "Next.js", "TypeScript", "Appwrite"], appStoreLabel: "Download on the App Store", appStoreUrl: carBrainLinks?.appStoreUrl ?? "", siteLabel: "car-brain.com", @@ -48,12 +57,6 @@ export const carBrain: CarBrain = { shotWidth: 392, shotHeight: 822, shots: [ - { - id: "analytics", - label: "Analytics", - src: "/work/car-brain/cb-analytics.webp", - alt: "Car Brain analytics screen with fuel-efficiency chart", - }, { id: "refuels", label: "Refuels", @@ -74,3 +77,43 @@ export const carBrain: CarBrain = { }, ], }; + +export const carBrainFor = (locale: "en" | "pl"): CarBrain => { + if (locale === "en") return carBrain; + const shots = { + refuels: { + label: "Tankowania", + alt: "Car Brain: historia tankowań, kosztów i zużycia paliwa", + }, + services: { + label: "Serwis", + alt: "Car Brain: historia serwisowa według kategorii", + }, + themes: { + label: "Motywy", + alt: "Car Brain: ustawienia motywów, w tym trybu nocnej jazdy", + }, + }; + return { + ...carBrain, + eyebrow: "Własny produkt", + role: "Twórca i programista", + text: "Rozwijam aplikację do zarządzania pojazdami na iOS, Androida i przeglądarkę, ze wspólnym backendem Appwrite.", + architecture: + "Aplikacja mobilna korzysta z React Native, a powstająca wersja webowa z Next.js. Obie używają TypeScriptu. Funkcje chmurowe obsługują analizę AI i dekodowanie VIN.", + features: + "Zarządzanie pojazdami · Serwis, paliwo i koszty · Raporty i analityka", + automation: + "Analiza danych pojazdu z AI · Dekodowanie VIN i automatyczne uzupełnianie formularza", + status: + "Trwają prace nad przywróceniem aplikacji mobilnej i rozwojem wersji webowej.", + articleLabel: "Dlaczego tworzę Car Brain", + screensLabel: "Ekrany Car Brain", + sampleNote: "Ekrany aplikacji z przykładowymi danymi.", + appStoreLabel: "Pobierz z App Store", + shots: carBrain.shots.map((shot) => ({ + ...shot, + ...shots[shot.id as keyof typeof shots], + })), + }; +}; diff --git a/src/sections/Now/CarBrainShots.tsx b/src/sections/Now/CarBrainShots.tsx index cd901d0..e5e1532 100644 --- a/src/sections/Now/CarBrainShots.tsx +++ b/src/sections/Now/CarBrainShots.tsx @@ -5,13 +5,21 @@ import type { CarBrainShot } from "../../content/carBrain"; type CarBrainShotsProps = { shots: CarBrainShot[]; + label: string; + caption: string; width: number; height: number; }; // A phone screenshot with buttons that swap the screen. The image keeps its // width and height attributes, so swapping never shifts the layout. -export const CarBrainShots = ({ shots, width, height }: CarBrainShotsProps) => { +export const CarBrainShots = ({ + shots, + width, + height, + label, + caption, +}: CarBrainShotsProps) => { const [currentId, setCurrentId] = useState(shots[0]?.id); const current = shots.find((shot) => shot.id === currentId) ?? shots[0]; @@ -37,7 +45,7 @@ export const CarBrainShots = ({ shots, width, height }: CarBrainShotsProps) => { /> -
+
{shots.map((shot) => ( ))}
+
{caption}
); }; diff --git a/src/sections/Now/NowSection.test.tsx b/src/sections/Now/NowSection.test.tsx index 43e8d39..3efbd1a 100644 --- a/src/sections/Now/NowSection.test.tsx +++ b/src/sections/Now/NowSection.test.tsx @@ -17,15 +17,15 @@ describe("while Car Brain is unpublished", () => { } }); - test("only TheEventa renders, across the full row", () => { + test("both development cards render before store availability", () => { const { container } = render(); expect( screen.getByRole("article", { name: profile.building.title }) ).toBeInTheDocument(); expect( screen.queryByRole("article", { name: carBrain.title }) - ).not.toBeInTheDocument(); - expect(container.firstChild).toHaveClass("now-grid--single"); + ).toBeInTheDocument(); + expect(container.firstChild).not.toHaveClass("now-grid--single"); }); test("no store or site link reaches the page", () => { @@ -82,3 +82,19 @@ test("TheEventa links to its case study", () => { screen.getByRole("link", { name: /How I run delivery/ }) ).toHaveAttribute("href", caseStudyPath("theeventa-mvp").replace(/\/$/, "")); }); + +test("Polish scope and article link are localized without store claims", () => { + render(); + expect( + screen.getByText(/Dekodowanie VIN i automatyczne/) + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /Dlaczego tworzę Car Brain/ }) + ).toHaveAttribute("href", "/blog/pl/why-i-built-car-brain"); + expect( + screen.getByRole("group", { name: "Ekrany Car Brain" }) + ).toBeInTheDocument(); + expect( + screen.queryByRole("link", { name: /Pobierz/ }) + ).not.toBeInTheDocument(); +}); diff --git a/src/sections/Now/NowSection.tsx b/src/sections/Now/NowSection.tsx index 1104a92..6319261 100644 --- a/src/sections/Now/NowSection.tsx +++ b/src/sections/Now/NowSection.tsx @@ -2,10 +2,7 @@ import React from "react"; import Link from "next/link"; import { getProfile } from "@/i18n/profile"; import type { Locale } from "@/i18n/locales"; -import { - carBrain as carBrainContent, - type CarBrain, -} from "../../content/carBrain"; +import { carBrainFor, type CarBrain } from "../../content/carBrain"; import { caseStudyFor, caseStudyPath } from "../../content/work/studies"; import { CarBrainShots } from "./CarBrainShots"; @@ -66,7 +63,7 @@ const Building = ({ locale = "en" }: { locale?: Locale }) => { ); }; -const Shipped = ({ +const IndependentProduct = ({ carBrain, locale = "en", }: { @@ -81,42 +78,53 @@ const Shipped = ({

{carBrain.eyebrow} - {carBrain.years} + {carBrain.role}

{carBrain.title}

{carBrain.text}

-
    - {carBrain.tags.map((tag) => ( -
  • - {tag} -
  • - ))} -
+
+
+
Stack
+
{carBrain.tags.join(" · ")}
+
+
+
{locale === "pl" ? "Główne funkcje" : "Key features"}
+
{carBrain.features}
+
+
+
{locale === "pl" ? "AI i automatyzacja" : "AI & automation"}
+
{carBrain.automation}
+
+
+

{carBrain.architecture}

+

+ Status: + {carBrain.status} +

- - {carBrain.appStoreLabel} - - - {carBrain.siteLabel} - + {carBrain.articleLabel} + + {carBrain.published && carBrain.appStoreUrl && ( + + {carBrain.appStoreLabel} + + )}

); -// TheEventa, which is still an MVP, and Car Brain, which ships only once its -// publication gate is open. Without Car Brain, TheEventa spans the row. +// Product visibility is separate from App Store availability. export const NowSection = ({ - carBrain = carBrainContent, + carBrain, locale = "en", }: { carBrain?: CarBrain; locale?: Locale; }) => ( -
+
- {carBrain.published && } +
); diff --git a/src/sections/Now/now.css b/src/sections/Now/now.css index e89faf5..31e6732 100644 --- a/src/sections/Now/now.css +++ b/src/sections/Now/now.css @@ -1,6 +1,4 @@ -/* Now building (TheEventa, a dark band card) and shipped on my own (Car - Brain, a paper card with a phone). Stacked on phones; 0.8 : 1.2 on wide - screens. Without Car Brain, TheEventa spans the row. */ +/* TheEventa and independent Car Brain development. Stacked on phones. */ .now-grid { display: grid; gap: 16px; @@ -245,3 +243,28 @@ gap: 28px; } } + +.now-details { + display: grid; + gap: 12px; + margin: 0; +} +.now-details dt { + color: var(--text); + font-size: 13px; + font-weight: 600; +} +.now-details dd { + margin: 3px 0 0; + color: var(--text-soft); + font-size: 14px; + line-height: 1.5; + overflow-wrap: anywhere; +} +.now-shot-caption { + max-width: 30ch; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.5; + text-align: center; +}