- {/* A plain img: the site is a static export with no image optimiser. */}
- {/* eslint-disable-next-line @next/next/no-img-element */}
-
-
-
{shot.caption}
-
+ {row[locale].map((shot) => (
+
))}
diff --git a/src/components/Blog/blog.css b/src/components/Blog/blog.css
index f2dee77..357bc1c 100644
--- a/src/components/Blog/blog.css
+++ b/src/components/Blog/blog.css
@@ -114,6 +114,10 @@
margin: 32px 0;
min-width: 0;
}
+.product-screens-hero {
+ display: flex;
+ justify-content: center;
+}
.product-screens-row {
display: flex;
gap: 12px;
diff --git a/src/content/blog/articles/why-i-built-car-brain/en.mdx b/src/content/blog/articles/why-i-built-car-brain/en.mdx
index c99d56f..0365b4c 100644
--- a/src/content/blog/articles/why-i-built-car-brain/en.mdx
+++ b/src/content/blog/articles/why-i-built-car-brain/en.mdx
@@ -5,13 +5,19 @@ import { ProductScreens } from "@/components/Blog/ProductScreens";
Fuel, service, costs and receipts were scattered. A spreadsheet is a poor filing cabinet for photos. I also wanted to learn mobile by shipping a whole product, not by finishing a tutorial. Car Brain is that product: React Native for iOS and Android, built in 2024–2025, which I am bringing back now.
+
+
## What I chose, and the bill
### React Native, through Expo
I already knew React and TypeScript. A second language was a worse deal than one codebase for two phones.
-**Gain:** iOS and Android from the same project, in tools I use every day. **Cost:** a dev client, native projects, and the reminder that the two platforms still disagree. **I would revisit it** if a feature needed a native interface I could only reach by fighting the abstraction.
+**Gain:** iOS and Android from the same project, in tools I use every day.
+
+**Cost:** a dev client, native projects, and the reminder that the two platforms still disagree.
+
+**I would revisit it** if a feature needed a native interface I could only reach by fighting the abstraction.
### Appwrite, instead of a server I would have to babysit
@@ -19,7 +25,11 @@ I wanted sign-in, structured records, file storage and a small place for server
-**Gain:** email, Google and Apple sign-in, tables, a bucket for photos and receipts, and functions. The mobile SDK talks to Appwrite directly. **Cost:** product rules live on the device or inside a function, and the schema lives in Appwrite's console as well as in the repo. Those two copies can drift. **I would revisit it** if the domain logic outgrew functions and deserved a backend I actually operate.
+**Gain:** email, Google and Apple sign-in, tables, a bucket for photos and receipts, and functions. The mobile SDK talks to Appwrite directly.
+
+**Cost:** product rules live on the device or inside a function, and the schema lives in Appwrite's console as well as in the repo. Those two copies can drift.
+
+**I would revisit it** if the domain logic outgrew functions and deserved a backend I actually operate.
For a mobile product, and if you already write React, I would pick this pair again. Expo is how I would reach both stores without a second language. Appwrite is the piece I would recommend most plainly: sign-in, a database, files, and a function for anything that must not ship inside the app, without a server to run. I would not call it better than Firebase or Supabase in general. I would start with it again for an app like this.
@@ -29,13 +39,21 @@ One scar from that setup is a separate story. An agent put a third-party API key
Data that comes from Appwrite goes through TanStack Query. The session, language and theme stay in React Context. Preferences that belong to the device stay in AsyncStorage. I did not add Redux or Zustand. The first plan had been Context and AsyncStorage alone, which meant writing caching and invalidation by hand.
-**Gain:** a cache with an explicit way to mark it stale and refetch. **Cost:** two homes for state, and dashboard numbers computed on the phone from cached rows instead of stored a second time. **I would revisit a global store** only if those homes started disagreeing.
+**Gain:** a cache with an explicit way to mark it stale and refetch.
+
+**Cost:** two homes for state, and dashboard numbers computed on the phone from cached rows instead of stored a second time.
+
+**I would revisit a global store** only if those homes started disagreeing.
### Maestro, after Detox stayed a name in a plan
The early notes named Detox or Appium for end-to-end tests. Neither was added. The repository runs Maestro flows against a development build, aimed at `testID`s.
-**Gain:** readable YAML and no extra native test harness inside the app. **Cost:** a local CLI, a booted simulator, and flows that are not what CI runs. The decision record does not say why Maestro won, so I will not invent a reason after the fact. **I would revisit it** when those flows need to run unattended.
+**Gain:** readable YAML and no extra native test harness inside the app.
+
+**Cost:** a local CLI, a booted simulator, and flows that are not what CI runs. The decision record does not say why Maestro won, so I will not invent a reason after the fact.
+
+**I would revisit it** when those flows need to run unattended.
## What is actually in the app
diff --git a/src/content/blog/articles/why-i-built-car-brain/pl.mdx b/src/content/blog/articles/why-i-built-car-brain/pl.mdx
index 7563706..456556f 100644
--- a/src/content/blog/articles/why-i-built-car-brain/pl.mdx
+++ b/src/content/blog/articles/why-i-built-car-brain/pl.mdx
@@ -5,13 +5,19 @@ import { ProductScreens } from "@/components/Blog/ProductScreens";
Paliwo, serwis, koszty i paragony miałem porozrzucane. Arkusz kalkulacyjny słabo nadaje się do trzymania zdjęć. Chciałem też nauczyć się aplikacji mobilnych na prawdziwym produkcie, a nie na kolejnym tutorialu. Car Brain jest właśnie tą aplikacją: React Native na iOS i Androida. Zbudowałem ją w latach 2024–2025 i teraz do niej wracam.
+
+
## Co wybrałem i ile mnie to kosztuje
### React Native przez Expo
Reacta i TypeScript już znałem. Uczenie się drugiego języka byłoby gorszym pomysłem niż jedna baza kodu na dwa telefony.
-**Zysk:** iOS i Android z jednego projektu, w narzędziach, których używam na co dzień. **Koszt:** klient deweloperski, projekty natywne i przypomnienie, że obie platformy nadal mają własne zdanie. **Zmieniłbym tę decyzję,** gdyby jakaś funkcja wymagała natywnego ekranu, a Expo tylko stało na drodze.
+**Zysk:** iOS i Android z jednego projektu, w narzędziach, których używam na co dzień.
+
+**Koszt:** klient deweloperski, projekty natywne i przypomnienie, że obie platformy nadal mają własne zdanie.
+
+**Zmieniłbym tę decyzję,** gdyby jakaś funkcja wymagała natywnego ekranu, a Expo tylko stało na drodze.
### Appwrite zamiast serwera, który musiałbym sam utrzymywać
@@ -19,7 +25,11 @@ Chciałem logowanie, dane o aucie, pliki i trochę kodu po stronie serwera. Appw
-**Zysk:** logowanie e-mailem, kontem Google i Apple, tabele, miejsce na zdjęcia i paragony oraz funkcje. Mobilne SDK rozmawia z Appwrite bezpośrednio. **Koszt:** reguły produktu siedzą albo w telefonie, albo w funkcji, a schemat jest i w konsoli Appwrite, i w repozytorium. Te dwie kopie potrafią się rozjechać. **Zmieniłbym tę decyzję,** gdyby logika produktu przestała mieścić się w funkcjach i potrzebowała backendu, który naprawdę utrzymuję.
+**Zysk:** logowanie e-mailem, kontem Google i Apple, tabele, miejsce na zdjęcia i paragony oraz funkcje. Mobilne SDK rozmawia z Appwrite bezpośrednio.
+
+**Koszt:** reguły produktu siedzą albo w telefonie, albo w funkcji, a schemat jest i w konsoli Appwrite, i w repozytorium. Te dwie kopie potrafią się rozjechać.
+
+**Zmieniłbym tę decyzję,** gdyby logika produktu przestała mieścić się w funkcjach i potrzebowała backendu, który naprawdę utrzymuję.
Skoro i tak piszę w Reakcie, do takiej aplikacji wziąłbym tę parę jeszcze raz. Expo jest sposobem na oba sklepy bez drugiego języka. Appwrite polecam wprost: logowanie, baza, pliki i funkcja na wszystko, czego nie wolno wkładać do aplikacji. Nie trzeba do tego stawiać własnego serwera. Nie mówię, że jest lepszy od Firebase albo Supabase w każdej sytuacji. Przy takiej aplikacji zacząłbym od niego znowu.
@@ -29,13 +39,21 @@ Została z tego też jedna wpadka. O niej jest osobny tekst. Agent zapisał kluc
Dane z Appwrite idą przez TanStack Query. Sesja, język i motyw zostają w React Context. Ustawienia telefonu zostają w AsyncStorage. Nie dodałem Reduxa ani Zustand. Pierwszy plan zakładał sam Context i AsyncStorage, czyli cache i odświeżanie pisane ręcznie.
-**Zysk:** cache, w którym widać, kiedy dane są już stare, i jest sposób, żeby pobrać je ponownie. **Koszt:** stan jest w dwóch miejscach, a liczby na ekranie głównym liczę w telefonie z wierszy w cache, zamiast zapisywać je drugi raz. **Wspólny store wziąłbym** dopiero wtedy, gdy te dwa miejsca zaczęłyby sobie przeczyć.
+**Zysk:** cache, w którym widać, kiedy dane są już stare, i jest sposób, żeby pobrać je ponownie.
+
+**Koszt:** stan jest w dwóch miejscach, a liczby na ekranie głównym liczę w telefonie z wierszy w cache, zamiast zapisywać je drugi raz.
+
+**Wspólny store wziąłbym** dopiero wtedy, gdy te dwa miejsca zaczęłyby sobie przeczyć.
### Maestro, kiedy Detox został tylko nazwą w planie
Wczesne notatki do testów E2E wymieniały Detox albo Appium. Żadnego z nich nie dodałem. Repozytorium odpala scenariusze Maestro na buildzie deweloperskim i szuka elementów po `testID`.
-**Zysk:** czytelny YAML, bez dokładania natywnej biblioteki testowej do aplikacji. **Koszt:** lokalne CLI, włączony symulator i scenariusze, których CI nie odpala. W zapisie decyzji nie ma, dlaczego wygrało Maestro, więc nie dopiszę powodu po fakcie. **Zmieniłbym ten wybór,** gdyby te testy miały lecieć same, bez ręcznego odpalania.
+**Zysk:** czytelny YAML, bez dokładania natywnej biblioteki testowej do aplikacji.
+
+**Koszt:** lokalne CLI, włączony symulator i scenariusze, których CI nie odpala. W zapisie decyzji nie ma, dlaczego wygrało Maestro, więc nie dopiszę powodu po fakcie.
+
+**Zmieniłbym ten wybór,** gdyby te testy miały lecieć same, bez ręcznego odpalania.
## Co naprawdę jest w aplikacji