Skip to content
Merged
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
42 changes: 32 additions & 10 deletions e2e/blog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { visiblePosts } from "../src/content/blog/.generated/posts";
import { blogArticlePath } from "../src/content/blog/model";
import { blogLabels } from "../src/content/blog/labels";
const first = visiblePosts[0];
const architectureSlug =
"building-mobile-and-web-clients-around-one-appwrite-backend";
const introSlug = "why-i-built-car-brain";
const articlePaths = visiblePosts.flatMap((p) =>
(["en", "pl"] as const).map((locale) => ({
path: blogArticlePath(p.slug, locale),
Expand Down Expand Up @@ -76,11 +79,11 @@ for (const skin of ["standard", "terminal"] as const)
const paths = [
"/blog/",
"/blog/pl/",
...(first
? (["en", "pl"] as const).map((locale) =>
blogArticlePath(first.slug, locale)
)
: []),
...visiblePosts.flatMap((post) =>
(["en", "pl"] as const).map((locale) =>
blogArticlePath(post.slug, locale)
)
),
];
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
Expand All @@ -94,7 +97,20 @@ for (const skin of ["standard", "terminal"] as const)
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual(
[]
);
if (path.includes(first?.slug ?? "__none__")) {
if (path.includes(introSlug)) {
await expect(page.locator(".product-screens img")).toHaveCount(3);
await expect(page.locator(".product-screens-row")).toHaveCSS(
"overflow-x",
"auto"
);
const stack = page.locator(".mobile-stack");
await expect(stack.getByRole("img")).toBeVisible();
await expect(stack.getByRole("region")).toHaveCSS(
"overflow-x",
"auto"
);
}
if (path.includes(architectureSlug)) {
// Diagram geometry must survive missing component CSS.
for (const selector of [".client-architecture", ".auth-sequence"]) {
const diagram = page.locator(selector);
Expand Down Expand Up @@ -144,8 +160,14 @@ test("blog discovery and keyboard navigation reach the index and both editions",
})),
];
for (const { query, path } of destinations) {
await page.getByRole("button", { name: "Open command palette" }).click();
await page.getByRole("combobox", { name: "Search commands" }).fill(query);
await page
.getByRole("button", {
name: /Open command palette|Otwórz paletę poleceń/,
})
.click();
await page
.getByRole("combobox", { name: /Search commands|Szukaj poleceń/ })
.fill(query);
await page.keyboard.press("Enter");
await expect(page).toHaveURL(new RegExp(path + "$"));
}
Expand Down Expand Up @@ -173,7 +195,7 @@ for (const [skin, theme, width] of [
await page.setViewportSize({ width, height: 1000 });
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto(
`${blogArticlePath(first.slug, width < 800 ? "pl" : "en")}?skin=${skin}&theme=${theme}`
`${blogArticlePath(architectureSlug, width < 800 ? "pl" : "en")}?skin=${skin}&theme=${theme}`
);
await page.screenshot({
path: `test-results/review-${skin}-${theme}-${width}-header.png`,
Expand All @@ -190,7 +212,7 @@ test("article diagrams keep their geometry without external stylesheets", async
}) => {
test.skip(!first, "No article in this build");
await page.setViewportSize({ width: 390, height: 900 });
await page.goto(blogArticlePath(first.slug, "pl"));
await page.goto(blogArticlePath(architectureSlug, "pl"));
await page.evaluate(() => {
for (const sheet of Array.from(document.styleSheets)) sheet.disabled = true;
});
Expand Down
Binary file added public/blog/why-i-built-car-brain/refuels.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/blog/why-i-built-car-brain/services.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/blog/why-i-built-car-brain/settings.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions src/components/Blog/Blog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { ClientArchitecture } from "./ClientArchitecture";
import { MobileStack } from "./MobileStack";
import { ProductScreens } from "./ProductScreens";
import { BlogList, BlogLanguages, LatestWriting } from "./BlogList";
import { BlogIndex, blogIndexMetadata } from "./BlogIndex";
import { visiblePosts } from "@/content/blog/.generated/posts";
Expand All @@ -17,6 +19,15 @@ test("language switch keeps the same article", () => {
expect.stringMatching(/^\/blog\/en\/example\/?$/)
);
});
test("the title link does not wrap the language switch", () => {
const post = visiblePosts[0];
const { container } = render(<BlogList locale="en" posts={[post]} />);
const item = container.querySelector(".blog-list-item");
expect(item).toHaveClass("card");
const title = screen.getByRole("link", { name: post.editions.en.title });
expect(title.closest(".blog-languages")).toBeNull();
expect(item?.querySelector(".blog-languages a")).toBeTruthy();
});
test("empty article list has no placeholder cards", () => {
const { container } = render(<BlogList locale="en" posts={[]} />);
expect(container.querySelectorAll("li")).toHaveLength(0);
Expand Down Expand Up @@ -54,6 +65,18 @@ test("article metadata preserves independent canonicals and prevents draft index
expect(m.robots).toMatchObject({ index: false, follow: false });
});

test("mobile stack and sample screens stay in the article", () => {
const { container } = render(
<>
<MobileStack locale="en" />
<ProductScreens locale="en" />
</>
);
expect(
screen.getByRole("img", { name: /Car Brain mobile stack/ })
).toBeVisible();
expect(container.querySelectorAll(".product-screens img")).toHaveLength(3);
});
for (const locale of ["en", "pl"] as const) {
test(`architecture exposes its boundaries and planned data path (${locale})`, () => {
const { container } = render(<ClientArchitecture locale={locale} />);
Expand Down
18 changes: 14 additions & 4 deletions src/components/Blog/BlogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ import { SectionBand } from "@/components/SectionBand/SectionBand";
export function BlogLanguages({
locale,
slug,
label,
}: {
locale: BlogLocale;
slug?: string;
label?: string;
}) {
return (
<nav
className="blog-languages"
aria-label={
slug ? blogLabels[locale].language : blogLabels[locale].indexLanguage
label ??
(slug ? blogLabels[locale].language : blogLabels[locale].indexLanguage)
}
>
{(["en", "pl"] as const).map((lang) => (
Expand Down Expand Up @@ -51,7 +54,7 @@ export function BlogList({
return (
<ul className="blog-list">
{posts.map((post) => (
<li key={post.slug} className="blog-list-item">
<li key={post.slug} className="card blog-list-item">
<p className="blog-meta">
{post.publishedAt ? (
<time dateTime={post.publishedAt}>
Expand All @@ -64,14 +67,21 @@ export function BlogList({
{post.readingMinutes[locale]} {t.minute}
</p>
<Heading>
<Link href={blogArticlePath(post.slug, locale)}>
<Link
className="blog-list-link"
href={blogArticlePath(post.slug, locale)}
>
{post.editions[locale].title}
</Link>
</Heading>
<p className="blog-description">
{post.editions[locale].description}
</p>
<BlogLanguages locale={locale} slug={post.slug} />
<BlogLanguages
locale={locale}
slug={post.slug}
label={`${t.language}: ${post.editions[locale].title}`}
/>
</li>
))}
</ul>
Expand Down
140 changes: 140 additions & 0 deletions src/components/Blog/MobileStack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { useId } from "react";
import type { BlogLocale } from "@/content/blog/types";

const ink = "var(--text, #1c282c)";
const muted = "var(--text-secondary, #52646a)";
const accent = "var(--accent, #0f6b75)";
const surface = "var(--surface, #ffffff)";
const soft = "var(--accent-soft, #e2efee)";
const border = "var(--separator, #d4deda)";

export function MobileStack({ locale = "en" }: { locale?: BlogLocale }) {
const pl = locale === "pl";
const id = useId();
const title = pl ? "Stos mobilny Car Brain" : "Car Brain mobile stack";
const description = pl
? "Aplikacja Expo na iOS i Androida rozmawia z Appwrite: logowanie, tabele, pliki i funkcje. W oryginalnej aplikacji nie ma osobnego serwera API."
: "The Expo app on iOS and Android talks to Appwrite for auth, tables, files and functions. There is no separate API server in the original app.";
return (
<figure className="mobile-stack" style={{ margin: "32px 0", minWidth: 0 }}>
<div
tabIndex={0}
role="region"
aria-label={`${title} — ${pl ? "przewijany diagram" : "scrollable diagram"}`}
style={{
overflowX: "auto",
borderRadius: "var(--radius-surface, 16px)",
border: `1px solid ${border}`,
background: surface,
}}
>
<svg
data-diagram="stack"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 720 240"
width={720}
height={240}
role="img"
aria-labelledby={`${id}-title ${id}-desc`}
style={{
display: "block",
width: "100%",
minWidth: 640,
height: "auto",
fontFamily: "var(--font, system-ui, sans-serif)",
}}
>
<title id={`${id}-title`}>{title}</title>
<desc id={`${id}-desc`}>{description}</desc>
<defs>
<marker
id={`${id}-arrow`}
viewBox="0 0 10 10"
refX={9}
refY={5}
markerWidth={7}
markerHeight={7}
orient="auto-start-reverse"
>
<path
d="M1 1L9 5L1 9"
fill="none"
stroke={accent}
strokeWidth={1.5}
/>
</marker>
</defs>
<rect width={720} height={240} fill={surface} />
<rect
x={28}
y={36}
width={230}
height={150}
rx={12}
fill={surface}
stroke={border}
strokeWidth={1.5}
/>
<text
x={143}
y={100}
textAnchor="middle"
fill={ink}
fontSize={18}
fontWeight={650}
>
{pl ? "Aplikacja Expo" : "Expo app"}
</text>
<text x={143} y={128} textAnchor="middle" fill={muted} fontSize={15}>
iOS {pl ? "i" : "and"} Android
</text>
<path
d="M258 111 H308"
fill="none"
stroke={accent}
strokeWidth={1.7}
markerEnd={`url(#${id}-arrow)`}
/>
<text x={283} y={96} textAnchor="middle" fill={muted} fontSize={13}>
SDK
</text>
<rect
x={310}
y={28}
width={382}
height={166}
rx={12}
fill={soft}
stroke={accent}
strokeWidth={2}
/>
<text
x={501}
y={78}
textAnchor="middle"
fill={accent}
fontSize={18}
fontWeight={650}
>
Appwrite
</text>
<text x={501} y={112} textAnchor="middle" fill={accent} fontSize={15}>
{pl
? "Logowanie · tabele · pliki · funkcje"
: "Auth · tables · files · functions"}
</text>
<text x={501} y={146} textAnchor="middle" fill={muted} fontSize={15}>
{pl ? "Bez mojego serwera API" : "No API server of my own"}
</text>
</svg>
</div>
<figcaption
style={{ marginTop: 12, color: muted, fontSize: 14, lineHeight: 1.6 }}
>
{pl
? "Telefon rozmawia z Appwrite bezpośrednio. Na małym ekranie przewiń schemat w poziomie."
: "The phone talks to Appwrite directly. On a narrow screen, scroll the diagram horizontally."}
</figcaption>
</figure>
);
}
77 changes: 77 additions & 0 deletions src/components/Blog/ProductScreens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { BlogLocale } from "@/content/blog/types";

const shots = {
en: [
{
src: "/blog/why-i-built-car-brain/refuels.jpg",
alt: "Refuel records in Car Brain",
caption: "Refuels, with search and a running cost summary.",
},
{
src: "/blog/why-i-built-car-brain/services.jpg",
alt: "Service records in Car Brain",
caption:
"Service history, split into what is done and what is coming up.",
},
{
src: "/blog/why-i-built-car-brain/settings.jpg",
alt: "Theme settings in Car Brain, including night driving",
caption: "Themes, including the red night-driving mode.",
},
],
pl: [
{
src: "/blog/why-i-built-car-brain/refuels.jpg",
alt: "Tankowania w Car Brain",
caption: "Lista tankowań z wyszukiwaniem i podsumowaniem kosztów.",
},
{
src: "/blog/why-i-built-car-brain/services.jpg",
alt: "Serwis w Car Brain",
caption:
"Historia serwisu: to, co już zrobione, i to, co dopiero nadchodzi.",
},
{
src: "/blog/why-i-built-car-brain/settings.jpg",
alt: "Ustawienia motywu w Car Brain, w tym jazda nocą",
caption: "Motywy, w tym czerwony tryb jazdy nocą.",
},
],
} as const;

export function ProductScreens({ locale = "en" }: { locale?: BlogLocale }) {
const pl = locale === "pl";
return (
<figure className="product-screens">
<div
className="product-screens-row"
tabIndex={0}
role="region"
aria-label={
pl
? "Ekrany aplikacji. Na wąskim ekranie przewiń w poziomie."
: "App screens. On a narrow screen, scroll sideways."
}
>
{shots[locale].map((shot) => (
<div className="product-screens-shot" key={shot.src}>
<div className="phone-tile">
{/* A plain img: the site is a static export with no image optimiser. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="phone-shot"
src={shot.src}
alt={shot.alt}
width={560}
height={1174}
loading="lazy"
decoding="async"
/>
</div>
<p>{shot.caption}</p>
</div>
))}
</div>
</figure>
);
}
Loading
Loading