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
21 changes: 12 additions & 9 deletions e2e/blog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,25 @@ for (const skin of ["standard", "terminal"] as const)
[]
);
if (path.includes(introSlug)) {
await expect(page.locator(".product-screens-hero img")).toHaveCount(
1
);
await expect(page.locator(".product-screens-hero")).toHaveCount(0);
await expect(page.locator(".product-screens-row img")).toHaveCount(
2
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"
);
const appwrite = stack.getByText("Appwrite", { exact: true });
await appwrite.scrollIntoViewIfNeeded();
await expect(appwrite).toBeVisible();
if (width < 700) {
const box = await appwrite.boundingBox();
expect(box?.x).toBeGreaterThanOrEqual(-1);
expect((box?.x ?? 0) + (box?.width ?? 0)).toBeLessThanOrEqual(
width + 1
);
}
}
if (path.includes(architectureSlug)) {
// Diagram geometry must survive missing component CSS.
Expand Down
10 changes: 4 additions & 6 deletions src/components/Blog/Blog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,16 @@ test("mobile stack and sample screens stay in the article", () => {
const { container } = render(
<>
<MobileStack locale="en" />
<ProductScreens locale="en" variant="hero" />
<ProductScreens locale="en" />
</>
);
expect(
screen.getByRole("img", { name: /Car Brain mobile stack/ })
screen.getByRole("figure", { name: /Car Brain mobile stack/ })
).toBeVisible();
expect(container.querySelectorAll(".product-screens-hero img")).toHaveLength(
1
);
expect(screen.getByText("Appwrite")).toBeVisible();
expect(container.querySelector(".product-screens-hero")).toBeNull();
expect(container.querySelectorAll(".product-screens-row img")).toHaveLength(
2
3
);
});
for (const locale of ["en", "pl"] as const) {
Expand Down
162 changes: 39 additions & 123 deletions src/components/Blog/MobileStack.tsx
Original file line number Diff line number Diff line change
@@ -1,140 +1,56 @@
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 canvas = "var(--canvas, #f7f5f0)";
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={canvas}
stroke={muted}
strokeWidth={1.5}
/>
<text
x={143}
y={100}
textAnchor="middle"
fill={ink}
fontSize={18}
fontWeight={650}
>
<figure className="mobile-stack" aria-label={title}>
<div className="mobile-stack-layout">
<div className="mobile-stack-node mobile-stack-expo">
<p className="mobile-stack-title">
{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}
</p>
<p className="mobile-stack-muted">iOS {pl ? "i" : "and"} Android</p>
</div>
<div className="mobile-stack-arrow" aria-hidden="true">
<span className="mobile-stack-sdk">SDK</span>
<svg
className="mobile-stack-arrow-icon"
width={28}
height={16}
viewBox="0 0 28 16"
focusable="false"
>
Appwrite
</text>
<text x={501} y={112} textAnchor="middle" fill={accent} fontSize={15}>
<path
d="M1 8 H20"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
/>
<path
d="M16 3 L23 8 L16 13"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
/>
</svg>
</div>
<div className="mobile-stack-node mobile-stack-appwrite">
<p className="mobile-stack-title">Appwrite</p>
<p>
{pl
? "Logowanie · tabele · pliki · funkcje"
: "Auth · tables · files · functions"}
</text>
<text x={501} y={146} textAnchor="middle" fill={muted} fontSize={15}>
</p>
<p className="mobile-stack-muted">
{pl ? "Bez mojego serwera API" : "No API server of my own"}
</text>
</svg>
</p>
</div>
</div>
<figcaption
style={{ marginTop: 12, color: muted, fontSize: 14, lineHeight: 1.6 }}
>
<figcaption>
{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."}
? "Telefon rozmawia z Appwrite bezpośrednio."
: "The phone talks to Appwrite directly."}
</figcaption>
</figure>
);
Expand Down
89 changes: 30 additions & 59 deletions src/components/Blog/ProductScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,13 @@ type Shot = {
caption: string;
};

const hero = {
en: {
src: "/blog/why-i-built-car-brain/dashboard.jpg",
alt: "Car Brain home screen for the current car",
caption:
"Home: the current car, fuel use, and what is due soon. The numbers and the car are sample data. On this capture the last-service time is clipped by the add button.",
},
pl: {
src: "/blog/why-i-built-car-brain/dashboard.jpg",
alt: "Ekran główny Car Brain dla aktualnego auta",
caption:
"Ekran główny: aktualne auto, spalanie i to, co wkrótce do zrobienia. Liczby i auto to dane przykładowe. Na tym kadrze godzina ostatniego serwisu jest ucięta przez przycisk dodawania.",
},
} as const satisfies Record<BlogLocale, Shot>;

const row = {
const shots = {
en: [
{
src: "/blog/why-i-built-car-brain/dashboard.jpg",
alt: "Car Brain home screen for the current car",
caption: "Home: the current car, fuel use, and what is due soon.",
},
{
src: "/blog/why-i-built-car-brain/refuels.jpg",
alt: "Refuel records in Car Brain",
Expand All @@ -36,6 +26,12 @@ const row = {
},
],
pl: [
{
src: "/blog/why-i-built-car-brain/dashboard.jpg",
alt: "Ekran główny Car Brain dla aktualnego auta",
caption:
"Ekran główny: aktualne auto, spalanie i to, co wkrótce do zrobienia.",
},
{
src: "/blog/why-i-built-car-brain/refuels.jpg",
alt: "Tankowania w Car Brain",
Expand All @@ -50,47 +46,7 @@ const row = {
],
} as const satisfies Record<BlogLocale, readonly Shot[]>;

function PhoneShot({
shot,
loading = "lazy",
}: {
shot: Shot;
loading?: "eager" | "lazy";
}) {
return (
<div className="product-screens-shot">
<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={loading}
decoding="async"
/>
</div>
<p>{shot.caption}</p>
</div>
);
}

export function ProductScreens({
locale = "en",
variant = "row",
}: {
locale?: BlogLocale;
variant?: "hero" | "row";
}) {
if (variant === "hero") {
return (
<figure className="product-screens product-screens-hero">
<PhoneShot shot={hero[locale]} loading="eager" />
</figure>
);
}
export function ProductScreens({ locale = "en" }: { locale?: BlogLocale }) {
const pl = locale === "pl";
return (
<figure className="product-screens">
Expand All @@ -104,8 +60,23 @@ export function ProductScreens({
: "App screens. On a narrow screen, scroll sideways."
}
>
{row[locale].map((shot) => (
<PhoneShot shot={shot} key={shot.src} />
{shots[locale].map((shot, index) => (
<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={index === 0 ? "eager" : "lazy"}
decoding="async"
/>
</div>
<p>{shot.caption}</p>
</div>
))}
</div>
</figure>
Expand Down
Loading
Loading