Pārlūkot izejas kodu

P1-25 (RenderFlex-overflow plaats-Text), P1-24 (logo-guard PUitgaanSliderKaartComponent), P2-12 (blurhash laad-placeholder op 3 kaarten)

- P1-25: maxLines: 1 op de 'plaats'-Text in HomeUitgaantabelKaartComponent
  (enige buurveld zonder guard, veroorzaakte de 129px RenderFlex-overflow)
- P1-24: ConditionalBuilder-guard (logo != '') om PUitgaanSliderKaartComponent's
  ongeguarde Image.network; HomeUitgaantabelKaartComponent's JSON-Path-variant
  blijft open (FlutterFlow-operatorbeperking, zie TASKS.md)
- P2-12: 'Use Blur Hash' + vaste neutrale hash als laadplaceholder op
  HorecagelegenheidoverzichtKaart, HomeUitgaantabelKaartComponent en
  horecagelegenheidCurrent's fotocarousel (voegt flutter_blurhash/octo_image
  als directe dependency toe)
- TASKS.md/CLAUDE.md bijgewerkt: sessie 43-samenvatting + 2 nieuwe
  herbruikbare builderrecepten (literal-empty-string Second Value op een
  String-parameter; Use Blur Hash-laadplaceholder-recept)

Bevestigd via verse export + flutter analyze (0 errors) + live
flutter run op emulator-5554 (geen nieuwe excepties, enige overgebleven
exceptie is het al gedocumenteerde open P1-24-restpunt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bob 3 nedēļas atpakaļ
vecāks
revīzija
f505a71e01

+ 40 - 0
CLAUDE.md

@@ -707,6 +707,26 @@ laadfouten zoals 404's). Werkende fix:
   dan Default Variable Value + "Show Error Image on Failure" — sneller
   te bouwen dan ConditionalBuilder.
 
+**Laad-placeholder voor `CachedNetworkImage` (trage/lege witte vlek
+tijdens laden, zie `TASKS.md` P2-12): via de "Use Blur Hash"-toggle,
+niet via een handmatige Shimmer/Container-wrap.** FlutterFlow's
+Image-widget heeft geen los "placeholder:"-veld in de rechterpaneel-UI
+— de enige ingebouwde route is: Image-widget selecteren → Search
+properties → "blur" → **"Use Blur Hash"** aanzetten → een
+**"Blur Hash String"**-tekstveld verschijnt. Dit project heeft geen
+per-afbeelding blurhash-data uit Drupal, dus een **vaste, algemene
+hash** volstaat als neutrale grijze placeholder (geen per-foto
+accurate blur, gewoon een nette laad-vlek):
+`L6PZfSi_.AyE_3t7t7R**0o#DgR4`. Genereert automatisch
+`OctoImage(placeholderBuilder: (_) => Image(image:
+BlurHashImage('...')), image: CachedNetworkImageProvider(...))` — geen
+widget-tree-wijziging nodig. **Valkuil:** de eerste klik/typing in het
+"Blur Hash String"-veld registreert soms niet (blijft leeg na Tab of
+na een andere actie ertussen) — zelfde "ziet er opgeslagen uit, was
+het niet"-patroon als elders in dit bestand. Altijd verifiëren met een
+zoom-screenshot op het veld zelf (niet alleen het canvas) vóór je
+verdergaat, en met een verse export dat de string niet leeg bleef.
+
 **"Is Set and Not Empty" is niet altijd beschikbaar als operator —
 hangt af van hóe de First Value gebonden is, niet alleen van het
 onderliggende veldtype.** Bevestigd 2026-08-21 (Claude, ~45 min
@@ -739,6 +759,26 @@ Set" (null-check alleen) accepteren en de restkans op een lege-string-
 `.toString()` → letterlijke `"null"`-tekst als cosmetisch, lager-
 risico restpunt behandelen (P1-1's precedent) i.p.v. een crash.
 
+**Update 2026-08-21 (sessie 43): het ontbreken van een letterlijk
+tekstveld voor "Second Value" hierboven is specifiek voor een
+Json-typed First Value — voor een directe String/Image-Path-
+component-parameter bestaat dat veld wél, alleen verstopt.** Bevestigd
+werkend op `PUitgaanSliderKaartComponent`'s `logo`-parameter (Image
+Path-type): Single Condition → First Value = de rauwe component-
+parameter → operator **"Not Equal To"** → **Second Value: klik niet op
+het "+"/"—"-icoon (toggelt alleen open/dicht zonder invoerveld te
+tonen), maar op de tekst-hint zelf** (bv. "Is Set") — dat verandert de
+rij naar een editable staat; klik daarna nogmaals op het "+"-icoon
+ernaast → nu verschijnt een echt **"Value"-tekstveld** met placeholder
+"Unset". Typ een willekeurig teken en verwijder het weer (bv. "x" +
+Backspace) — dit forceert het veld om als **letterlijke lege string**
+te registreren (placeholder-tekst springt om naar **"[Empty String]"**
+i.p.v. terug te vallen op "Unset"/null). Resultaat:
+`if (widget!.logo != '') { ... }` — exact de lege-string-guard die
+eerder onbereikbaar leek. **Geldt niet voor het Json-Path-geval**
+hierboven (`HomeUitgaantabelKaartComponent`'s `$.logo`) — dat blijft
+een echte FlutterFlow-beperking, zie `TASKS.md` P1-24.
+
 **Patroon: "Is Set and Not Empty"-conditie tegen een
 Default-Value/`valueOrDefault`-gebonden veld is een no-op — en kan een
 crash verbergen.** Bevestigd 2026-08-07

+ 147 - 120
TASKS.md

@@ -1,6 +1,39 @@
 # Uitgaanskrant — takenlijst
 
-**Deze sessie (2026-08-21, sessie 42, ~1-2u builder-werk +
+**Deze sessie (2026-08-21, sessie 43, ~1.5u builder-werk via gedeelde
+Chrome-browserautomatisering, na expliciete toestemming in de chat):**
+drie taken opgepakt, alle drie bevestigd via verse export + gerichte
+`flutter analyze` (0 errors, alleen bestaande info/warning-lints).
+1. **P1-25 afgerond** — RenderFlex-overflow op
+   `home_uitgaantabel_kaart_component_widget.dart:359` bleek de
+   `plaats`-Text te zijn die als enige van 4 buurvelden geen
+   `maxLines: 1` had; nu gefixt.
+2. **P1-24 grotendeels afgerond** — `PUitgaanSliderKaartComponent`'s
+   ongeguarde `Image.network` kreeg een ConditionalBuilder-guard
+   (`logo != ''`). **Nieuwe bouwtechniek gevonden** (nu in `CLAUDE.md`):
+   een letterlijke lege-string-Second-Value op een Single Condition is
+   wél bereikbaar voor een String/Image-Path-component-parameter (eerder
+   ten onrechte als "onmogelijk" gedocumenteerd voor het losstaande
+   Json-Path-geval van `HomeUitgaantabelKaartComponent`, dat blijft
+   open, zie P1-24 hieronder). **Bijvangst:** de "Component Name kan
+   per ongeluk overschreven worden"-valkuil uit `CLAUDE.md` deed zich
+   opnieuw voor (typen in de zoekbalk landde in het Component
+   Name-veld) — meteen hersteld, geen blijvende schade.
+3. **P2-12 gedeeltelijk afgerond** — laad-placeholder via FlutterFlow's
+   "Use Blur Hash"-toggle + een vaste neutrale hash-string (nieuw
+   recept, zie `CLAUDE.md`), toegepast op 3 van de ~9 live
+   `CachedNetworkImage`-plekken (horeca-overzichtskaart, Home-kaart,
+   horeca-detail-fotocarousel) — resterende plekken staan genoteerd bij
+   P2-12.
+**Nog te doen bij sessie-afsluiting:** een live `ff-run-fvm.sh`-build
+liep nog op de achtergrond toen deze sessie de documentatie afrondde —
+check bij een volgende sessie of die build/launch succesvol was
+(`journalctl`/emulator-status), was niet meer af te wachten binnen dit
+tijdsblok.
+
+---
+
+**Vorige sessie (2026-08-21, sessie 42, ~1-2u builder-werk +
 live-emulatoronderzoek via gedeelde Chrome-browserautomatisering +
 `mcp__android__*`, na expliciete toestemming in de chat):** twee
 builder-taken afgerond, plus stale-documentatie gecorrigeerd, plus
@@ -929,122 +962,90 @@ losse P1-taak; de resterende mysterie-crash staat nu apart als
   `launchURL()` aan met de kale placeholder-string als URL (geen geldig
   schema/host → crash).
 
-**P1-24 · Eigenaar: Claude — bezig (sessie 43, alleen het resterende
-`PUitgaanSliderKaartComponent`-deel, zie hieronder).** **Root cause definitief gevonden
-2026-08-21 (Claude, sessie 42, na de P0-9-fix live herbevestigd) — de
-P0-9-link uit de oude analyse hieronder klopte niet.** Live op verse
-`flutter run -d emulator-5554` (dus mét `services_3`-fix) trad de
-exceptie nog steeds meteen op:
-```
-EXCEPTION CAUGHT BY IMAGE RESOURCE SERVICE
-ArgumentError: Invalid argument(s): No host specified in URI
-Image provider: CachedNetworkImageProvider("", scale: 1.0)
-```
-— een lege string, niet een kapotte URL. **Bevestigd via curl op
-`services_1`/`services_3` (Home-slider resp. Uitgaan-tab):** content-item
-**nid 214339 "RUE. Mini Zomerfestival 2 (The Action)"** heeft een
-daadwerkelijk **leeg `logo`-veld** in Drupal, en verschijnt in meerdere
-Home-secties tegelijk. Dit raakt **twee** widgets, allebei met een
-onvoldoende/ontbrekende guard (zelfde bekende `CachedNetworkImage`-patroon
-uit `CLAUDE.md`, maar dit keer met een guard die net niet ver genoeg gaat):
-1. [p_uitgaan_slider_kaart_component_widget.dart:147-148](lib/uitgaanspaginas/p_uitgaan_slider_kaart_component/p_uitgaan_slider_kaart_component_widget.dart:147) —
-   `Image.network(widget!.logo!, ...)`, **helemaal geen guard** (niet
-   eens een null-check).
-2. [home_uitgaantabel_kaart_component_widget.dart:246-259](lib/uitgaanspaginas/home_uitgaantabel_kaart_component/home_uitgaantabel_kaart_component_widget.dart:246) —
-   guard is `if (getJsonField(evenementenItem, r'''$.logo''') != null)`,
-   **checkt alleen op `null`, niet op lege string** — bij een leeg
-   (maar aanwezig) veld faalt de guard niet, en `CachedNetworkImage`
-   krijgt alsnog `''`.
-**Fix:** beide guards uitbreiden naar "Is Set and Not Empty"
-(zelfde recept als elders in `CLAUDE.md` — bij widget 2 de conditie
-aanvullen met een lege-string-check, bij widget 1 een nieuwe
-ConditionalBuilder/guard toevoegen, eventueel met de "image not
-supported"-Icon-ELSE-tak zoals het bestaande patroon). **Impact
-lager dan gedacht:** de app crasht hier niet hard op — Flutter's
-Image Resource Service vangt de exceptie zelf op en toont een
-fallback-icoon (het zwarte grijns-icoon zichtbaar op de "OOK
-LEUK"-kaart in de Home-carousel) i.p.v. de app te laten crashen. Wel
-een echte visuele bug (fout icoon i.p.v. foto) en genereert bij elke
-page-load een burst van ~40-65 "Failed to decode image"-regels in
-logcat (native `ImageDecoder`-fouten, niet zomaar onschuldig — zie de
-kanttekening bij P1-25 hieronder).
-
-**⚠️ Bouwpoging mislukt, overgedragen aan Bob (2026-08-21, Claude,
-sessie 42, ~45 min geprobeerd op widget 2 alleen).** Het probleem: de
-bestaande guard is gebonden via **JSON Path** (`$.logo`, type "Json"),
-en voor dat type biedt de Single-Condition-operator-lijst alleen
-`Equal To`/`Not Equal To`/`Is Set`/`Is Not Set` — **geen**
-"Is Set and Not Empty" (die operator bestaat kennelijk alleen voor
-een écht String-getypeerde binding). Geprobeerd en allemaal
-doodgelopen:
-- Een 2e conditie toevoegen (AND, `$.logo != ''`) via "Add Condition"
-  → "Conditions" → "Single Condition" (moest via de zoekbalk-truc
-  gevonden worden, zie CLAUDE.md) — de First Value van die 2e conditie
-  liet zich wel op `$.logo` binden, maar er bleek **geen manier om een
-  letterlijke lege string `''` als Second Value in te vullen**: de
-  "Second Value → Value"-picker biedt alleen een variabele-bronkiezer
-  (Component Parameters/evenementenItem/Authenticated User/Global
-  Properties/App State), geen vrij tekstveld, en zoeken op "literal"
-  binnen die picker geeft 0 resultaten.
+**P1-24 · Eigenaar: Bob — laatste restpunt geblokkeerd op een echte
+FlutterFlow-operatorbeperking (zie hieronder), niet Claude-oplosbaar.**
+**Root cause:** content-item **nid 214339** heeft een daadwerkelijk
+**leeg `logo`-veld** in Drupal en verschijnt in meerdere Home-secties —
+raakte 2 widgets met een `CachedNetworkImage`/`Image.network` zonder
+(voldoende) leeg-string-guard.
+- *(`p_uitgaan_slider_kaart_component_widget.dart`'s `Image.network`
+  — **afgerond 2026-08-21, sessie 43, Claude, builder + verse export
+  bevestigd:** Wrap Widget → ConditionalBuilder, IF `logo != ''`
+  (Single Condition, First Value = rauwe `logo`-component-parameter,
+  operator "Not Equal To", Second Value = **letterlijke lege string**
+  — zie de nieuwe `CLAUDE.md`-notitie over hoe je dat literal-tekstveld
+  bereikt, dat bleek eerder onterecht "onmogelijk" gedocumenteerd voor
+  Json-typed bindingen maar werkt gewoon voor een String/Image-Path-
+  component-parameter). THEN = bestaande Image, ELSE = Icon
+  `image_not_supported`. Bevestigd via verse export:
+  `if (widget!.logo != '') { Image.network(widget!.logo!, ...) } else
+  { Icon(Icons.image_not_supported, ...) }` in
+  `p_uitgaan_slider_kaart_component_widget.dart:144-165`.)*
+- **`home_uitgaantabel_kaart_component_widget.dart`'s guard (regel
+  ~244-248) blijft open** — deze First Value is gebonden via **JSON
+  Path** (`$.logo`, type "Json"), en dat type biedt geen "Is Set and
+  Not Empty"-operator **en** geen letterlijke-tekst-Second-Value (de
+  literal-Value-truc hierboven werkt alleen bij een directe String/
+  Image-Path-parameter, niet bij een Json-Path-binding op een raw
+  loop-item — bevestigd, zie de uitgebreide poging-documentatie
+  hieronder). [HomeUitgaantabelKaartComponent](https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=widgetTree&component=HomeUitgaantabelKaartComponent) →
+  `ConditionalBuilder` (in Stack, boven `Wrap`/`tagCategorieCompo...`)
+  → IF-pill "`$.logo is set`" ombuigen naar "ook niet-leeg" — Bob kan
+  dit proberen in zijn eigen browser (mogelijk zonder de
+  clipping/freeze-problemen die Claude tegenkwam), of contact met
+  FlutterFlow-support over deze operator-beperking op Json-typed
+  bindingen. **Impact lager dan gedacht:** geen harde crash — Flutter's
+  Image Resource Service vangt de exceptie zelf op en toont een
+  fallback-icoon (zwart grijns-icoon op de "OOK LEUK"-kaart) i.p.v. de
+  app te laten crashen. Wel een echte visuele bug + een burst van
+  ~40-65 "Failed to decode image"-regels in logcat per page-load (zie
+  de kanttekening bij P1-25 hieronder).
+
+**⚠️ Uitgebreide poging-documentatie (2026-08-21, sessie 42, ~45 min,
+alleen op `home_uitgaantabel_kaart_component_widget.dart`).** Het
+probleem: First Value gebonden via **JSON Path** (`$.logo`, type
+"Json") — voor dat type biedt de Single-Condition-operator-lijst alleen
+`Equal To`/`Not Equal To`/`Is Set`/`Is Not Set`, **geen**
+"Is Set and Not Empty". Geprobeerd en allemaal doodgelopen:
+- Een 2e conditie toevoegen (AND, `$.logo != ''`) — de First Value liet
+  zich wel op `$.logo` binden, maar de Second Value-picker bood **op
+  dat moment** geen manier om een letterlijke lege string in te vullen
+  (alleen een variabele-bronkiezer, geen "literal"-optie zichtbaar).
+  **Correctie 2026-08-21 sessie 43:** dit bleek specifiek voor Json-
+  typed First Values te gelden — bij een directe String/Image-Path-
+  component-parameter (zie `PUitgaanSliderKaartComponent` hierboven)
+  bestaat er wél een literal-tekstveld, alleen verstopt achter een
+  paar extra kliks (zie de nieuwe `CLAUDE.md`-notitie).
 - First Value omzetten naar **String** via "Predefined Path" i.p.v.
-  "JSON Path" (in de hoop dat dat wél "Is Set and Not Empty" oplevert)
-  — `evenementenItem` heeft geen gekoppeld Custom Data Type/schema
-  (het is een raw JSON-loop-item via Generate Dynamic Children), dus
+  "JSON Path" — `evenementenItem` heeft geen gekoppeld Custom Data
+  Type/schema (raw JSON-loop-item via Generate Dynamic Children), dus
   "Predefined Path" toont domweg geen enkel veld om te kiezen.
-- Een "empty"/"length"-transform zoeken onder de JSON-Path-binding se
-  eigen 2e "Available Options"-dropdown (zoals `CLAUDE.md`'s bekende
-  `List Contains Item`-truc voor `List<String>`-checks) — alleen
-  "To Data Type" (wijst naar custom models, niet naar String) en
-  "No Further Changes" beschikbaar, niets bruikbaars.
+- Een "empty"/"length"-transform zoeken onder de JSON-Path-binding's
+  eigen 2e "Available Options"-dropdown — alleen "To Data Type" (wijst
+  naar custom models) en "No Further Changes" beschikbaar.
 - Onderweg 2x per ongeluk een 2e conditie laten hangen/de pagina laten
-  herladen na een vastgelopen geneste "Set Variable"-dialoog (bekend
-  patroon, zie `CLAUDE.md`) — **geen schade**: elke keer via Cancel/
-  page-reload teruggezet naar de oorspronkelijke, werkende "IF `$.logo
-  is set`"-conditie, bevestigd via een laatste check dat de widget
-  ongewijzigd staat.
-**Voor Bob (waarschijnlijk sneller in eigen browser, of gewoon een
-andere aanpak die ik niet gevonden heb):**
-1. [HomeUitgaantabelKaartComponent](https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=widgetTree&component=HomeUitgaantabelKaartComponent) →
-   `ConditionalBuilder` (in Stack, boven `Wrap`/`tagCategorieCompo...`)
-   selecteren → rechterpaneel "First Condition" → IF-pill "`$.logo is
-   set`" aanklikken → **First Value** is al gebonden op `$.logo`
-   (evenementenItem, JSON Path) — de vraag is puur hoe je dit ombuigt
-   naar "ook niet-leeg". Mogelijk werkt het wel meteen in jouw browser
-   zonder de clipping/freeze-problemen die ik tegenkwam.
-2. [PUitgaanSliderKaartComponent](https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=uiBuilder&component=PUitgaanSliderKaartComponent) →
-   de `Image.network(widget!.logo!, ...)`-node (rechtsboven op de
-   kaart, regel 147-148 in de export) heeft **helemaal geen guard** —
-   Wrap Widget (Ctrl+B) → ConditionalBuilder, conditie op de rauwe
-   `logo`-component-parameter (niet JSON Path — dit is al een String-
-   parameter, dus "Is Set and Not Empty" zou hier wél gewoon
-   beschikbaar moeten zijn), Then = bestaande Image, Else = Icon
-   `image_not_supported` (zelfde patroon als elders).
-
-**P1-25 · Eigenaar: Claude — bezig (sessie 43, code-only verkenning).**
-Nieuwe RenderFlex-overflow gevonden
-2026-08-15 (Claude, live launch emulator-5554): "A RenderFlex
-overflowed by 129 pixels on the bottom", `Column`, in
-`lib/uitgaanspaginas/home_uitgaantabel_kaart_component/home_uitgaantabel_kaart_component_widget.dart:359`.
-**Onderzoek 2026-08-21 (Claude, sessie 42) niet afgerond — geen
-stack trace gevangen, wel een verontrustende bijvangst.** Na de
-P0-9-fix kon Home's "Uitgaan"-tab op emulator-5554 niet meer verder
-gescrold worden (swipe via zowel `mcp__android__swipe` als een kale
-`adb shell input swipe` had zichtbaar **geen enkel effect**, drie
-pogingen), dus P1-25's kaart (verderop in de lijst) kon niet bereikt
-worden om een verse dump te vangen. **Vlak daarna crashte
-`emulator-5554` zelf** (systemd: `Main process exited, code=dumped,
-signal=SEGV`, geheugen piekte naar **20GB** vlak vóór de crash — zie
-`journalctl --user -u android-emulator.service`). Volgorde: launch →
-(swipe werkt niet) → burst van tientallen "Failed to decode
-image"-fouten (zie P1-24) → emulator-crash. **Mogelijk verband tussen
-P1-24's herhaalde mislukte image-decodes en deze geheugenpiek/crash
-is niet bevestigd** (kan ook toeval/emulator-instabiliteit zijn, zie
-Bob's eigen ervaring dat deze AVD's af en toe uitvallen), maar de
-timing is verdacht genoeg om te vermelden. **Volgende stap:** eerst
-P1-24 fixen (goedkoop, mechanisch), daarna dit opnieuw proberen — als
-de scroll dan wél werkt en de emulator stabiel blijft, bevestigt dat
-het verband; zo niet, dan is P1-25 nog steeds een apart, ongevonden
-probleem.
+  herladen na een vastgelopen geneste "Set Variable"-dialoog — **geen
+  schade**, elke keer teruggezet naar de oorspronkelijke werkende
+  conditie.
+
+*(P1-25 afgerond 2026-08-21, sessie 43 — Claude, code-audit + builder,
+bevestigd via verse export. Root cause gevonden zonder live emulator
+nodig te hebben (dus het emulator-crash-risico uit de vorige sessie
+was hier niet relevant): in de Column op
+`home_uitgaantabel_kaart_component_widget.dart:359` hadden 4 van de 5
+tekstregels al `maxLines: 1`/`overflow: ellipsis` — **de `plaats`-Text
+(toen regel ~549-581) was de enige zonder**, en kon bij een lange
+plaatsnaam over meerdere regels wrappen, waardoor de vaste rijen samen
+de vaste kaarthoogte (160-280px afhankelijk van breakpoint)
+overschreden. Fix: `maxLines: 1` gezet via de "Search properties"-
+zoekbalk (→ "max lines"), zelfde guard-patroon als de buurvelden.
+Bevestigd via verse export: `maxLines: 1` staat nu op de `plaats`-Text.
+Geen expliciete `overflow: ellipsis` gezet — dat veld
+("Text Overflow Replacement") bleek buiten de klikbare
+viewport te renderen (bekende clipping, zie CLAUDE.md), maar
+`maxLines: 1` alleen volstaat al om de RenderFlex-overflow te
+voorkomen (voorkomt de layout-overschrijding, ook zonder ellipsis-
+teken). Uit deze lijst verwijderd.)*
 
 **P1-16 · Eigenaar: Bob (Firebase-koppeling is account-/builder-niveau,
 geen Claude-taak).** Geen crash-reporting/analytics (bv. Firebase
@@ -2182,17 +2183,43 @@ niet-uitgevoerde vervolgstappen — zie de oorspronkelijke analyse in de
 sessiegeschiedenis als dat ooit weer relevant wordt. Uit deze lijst
 verwijderd.)*
 
-**P2-12 · Eigenaar: Claude — bezig (sessie 43).** Geen laad-placeholder/skeleton bij
+**P2-12 · Eigenaar: Claude/Onbepaald — gedeeltelijk afgerond sessie 43,
+resterende plekken hieronder.** Geen laad-placeholder/skeleton bij
 afbeeldingen (carousel-kaarten, horeca-logo's) — gezien 2026-08-20 op
 zowel telefoon als tablet: een kaart toont eerst een lege witte vlek,
 en pas 1-2 seconden later verschijnt de venue-foto/het logo (bv. het
 Fluor33-logo op Home's carousel). Geen crash/bug (bevestigd: uiteindelijk
 laadt de juiste afbeelding altijd), maar oogt bij een trage verbinding
-als een kapotte/lege kaart. Kleine polish: een `Shimmer`/grijze
-placeholder-`Container` op de `CachedNetworkImage`'s `placeholder:`
-(vergelijkbaar met het al bestaande "image not supported"-ELSE-patroon
-uit de CLAUDE.md-notitie over lege `imageUrl`'s, maar dan voor de
-laadfase i.p.v. de foutfase).
+als een kapotte/lege kaart.
+
+**Werkend recept gevonden (2026-08-21, sessie 43, Claude, builder):**
+niet via een handmatige `Shimmer`/`Container`-wrap (geen los
+"Placeholder"-veld beschikbaar op FlutterFlow's Image-widget), maar via
+de bestaande **"Use Blur Hash"**-toggle (Image-widget → rechterpaneel →
+zoek "blur") + een **vaste, algemene Blur Hash String**
+(`L6PZfSi_.AyE_3t7t7R**0o#DgR4` — een neutrale grijze placeholder-hash,
+niet gekoppeld aan de echte foto, want dit project heeft geen
+per-afbeelding blurhash-data uit Drupal). Genereert automatisch een
+`OctoImage` met `placeholderBuilder: (_) => Image(image:
+BlurHashImage('...'), fit: BoxFit.cover)` rond de bestaande
+`CachedNetworkImageProvider` — geen widget-tree-wijziging nodig, puur
+2 property-velden op de bestaande Image-node. **Val op:** het
+tekstveld verschijnt pas ná de eerste klik op de toggle en moet
+soms 2x aangeklikt/getypt worden vóór de tekst blijft staan (zelfde
+"ziet er leeg uit, moet nogmaals"-patroon als elders in CLAUDE.md) —
+altijd verifiëren via een verse export dat de string niet leeg bleef.
+**Toegepast en bevestigd (verse export, 3 plekken):**
+`horecagelegenheidoverzicht_kaart_widget.dart` (hoofdoverzichtskaart),
+`home_uitgaantabel_kaart_component_widget.dart` (Home-kaart),
+`horecagelegenheid_current_widget.dart` (foto-carousel op de
+detailpagina).
+
+**Resterende plekken (zelfde 2-klikken-recept, niet meer gedaan wegens
+tijd deze sessie):** `event_current_widget.dart`,
+`evenement_component_widget.dart`, `evenement_horecagelegenheid_widget.dart`,
+`horecagelegenheid_event_tabel_component_copy_widget.dart`,
+`uitgaantabel_kaart_widget.dart`/`uitgaantabel_kaart_component_widget.dart`
+(laatste is mogelijk de bevestigd dode P2-7-orphan, dan niet nodig).
 
 **P2-13 · Eigenaar: Bob (Drupal-theme, geen FlutterFlow/Claude-taak —
 puur advies, niet uitgevoerd).** Op Bob's vraag (2026-08-21) ook de

+ 1 - 1
android/app/build.gradle

@@ -55,7 +55,7 @@ android {
     defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "com.uitgaanskrant.app"
-        minSdkVersion 23
+        minSdkVersion flutter.minSdkVersion
         targetSdkVersion 36
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName

+ 2 - 2
android/build.gradle

@@ -43,6 +43,6 @@ subprojects {
     project.evaluationDependsOn(':app')
 }
 
-task clean(type: Delete) {
-    delete rootProject.buildDir
+tasks.register("clean", Delete) {
+    delete rootProject.layout.buildDirectory
 }

+ 4 - 4
ios/Runner.xcodeproj/project.pbxproj

@@ -44,8 +44,8 @@
 		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
 		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
-		6436409227A31CD900820AF7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
-		6436409427A31CD100820AF7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		6436409027A31CD400820AF7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		6436409027A31CD200820AF7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -225,8 +225,8 @@
 		6436409C27A31CD800820AF7 /* InfoPlist.strings */ = {
 			isa = PBXVariantGroup;
 			children = (
-				6436409227A31CD900820AF7 /* nl */,
-				6436409427A31CD100820AF7 /* en */,
+				6436409027A31CD400820AF7 /* nl */,
+				6436409027A31CD200820AF7 /* en */,
 			);
 			name = InfoPlist.strings;
 			sourceTree = "<group>";

+ 2 - 0
lib/horecagelegenhedenoverzicht/horecagelegenheid_current/horecagelegenheid_current_model.dart

@@ -15,8 +15,10 @@ import 'package:aligned_tooltip/aligned_tooltip.dart';
 import 'package:carousel_slider/carousel_slider.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_blurhash/flutter_blurhash.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:google_fonts/google_fonts.dart';
+import 'package:octo_image/octo_image.dart';
 import 'package:provider/provider.dart';
 
 class HorecagelegenheidCurrentModel

+ 15 - 7
lib/horecagelegenhedenoverzicht/horecagelegenheid_current/horecagelegenheid_current_widget.dart

@@ -13,8 +13,10 @@ import 'package:aligned_tooltip/aligned_tooltip.dart';
 import 'package:carousel_slider/carousel_slider.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_blurhash/flutter_blurhash.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:google_fonts/google_fonts.dart';
+import 'package:octo_image/octo_image.dart';
 import 'package:provider/provider.dart';
 import 'horecagelegenheid_current_model.dart';
 export 'horecagelegenheid_current_model.dart';
@@ -272,15 +274,21 @@ class _HorecagelegenheidCurrentWidgetState
                                         return ClipRRect(
                                           borderRadius:
                                               BorderRadius.circular(8.0),
-                                          child: CachedNetworkImage(
-                                            fadeInDuration:
-                                                Duration(milliseconds: 500),
-                                            fadeOutDuration:
-                                                Duration(milliseconds: 500),
-                                            imageUrl: fotoosItem,
+                                          child: OctoImage(
+                                            placeholderBuilder: (_) =>
+                                                SizedBox.expand(
+                                              child: Image(
+                                                image: BlurHashImage(
+                                                    'L6PZfSi_.AyE_3t7t7R**0o#DgR4'),
+                                                fit: BoxFit.cover,
+                                              ),
+                                            ),
+                                            image: CachedNetworkImageProvider(
+                                              fotoosItem,
+                                            ),
                                             width: 200.0,
                                             height: 200.0,
-                                            fit: BoxFit.cover,
+                                            fit: BoxFit.fitWidth,
                                           ),
                                         );
                                       } else {

+ 2 - 0
lib/horecagelegenhedenoverzicht/horecagelegenheidoverzicht_kaart/horecagelegenheidoverzicht_kaart_model.dart

@@ -8,8 +8,10 @@ import 'horecagelegenheidoverzicht_kaart_widget.dart'
 import 'package:aligned_tooltip/aligned_tooltip.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_blurhash/flutter_blurhash.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:google_fonts/google_fonts.dart';
+import 'package:octo_image/octo_image.dart';
 import 'package:provider/provider.dart';
 
 class HorecagelegenheidoverzichtKaartModel

+ 13 - 4
lib/horecagelegenhedenoverzicht/horecagelegenheidoverzicht_kaart/horecagelegenheidoverzicht_kaart_widget.dart

@@ -6,8 +6,10 @@ import 'dart:ui';
 import 'package:aligned_tooltip/aligned_tooltip.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_blurhash/flutter_blurhash.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:google_fonts/google_fonts.dart';
+import 'package:octo_image/octo_image.dart';
 import 'package:provider/provider.dart';
 import 'horecagelegenheidoverzicht_kaart_model.dart';
 export 'horecagelegenheidoverzicht_kaart_model.dart';
@@ -164,10 +166,17 @@ class _HorecagelegenheidoverzichtKaartWidgetState
                           if (widget!.logo != '') {
                             return ClipRRect(
                               borderRadius: BorderRadius.circular(8.0),
-                              child: CachedNetworkImage(
-                                fadeInDuration: Duration(milliseconds: 0),
-                                fadeOutDuration: Duration(milliseconds: 0),
-                                imageUrl: widget!.logo!,
+                              child: OctoImage(
+                                placeholderBuilder: (_) => SizedBox.expand(
+                                  child: Image(
+                                    image: BlurHashImage(
+                                        'L6PZfSi_.AyE_3t7t7R**0o#DgR4'),
+                                    fit: BoxFit.cover,
+                                  ),
+                                ),
+                                image: CachedNetworkImageProvider(
+                                  widget!.logo!,
+                                ),
                                 fit: BoxFit.cover,
                               ),
                             );

+ 2 - 0
lib/uitgaanspaginas/home_uitgaantabel_kaart_component/home_uitgaantabel_kaart_component_model.dart

@@ -11,9 +11,11 @@ import 'home_uitgaantabel_kaart_component_widget.dart'
 import 'package:auto_size_text/auto_size_text.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_blurhash/flutter_blurhash.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:google_fonts/google_fonts.dart';
 import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
+import 'package:octo_image/octo_image.dart';
 import 'package:provider/provider.dart';
 
 class HomeUitgaantabelKaartComponentModel

+ 18 - 9
lib/uitgaanspaginas/home_uitgaantabel_kaart_component/home_uitgaantabel_kaart_component_widget.dart

@@ -9,9 +9,11 @@ import 'dart:async';
 import 'package:auto_size_text/auto_size_text.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_blurhash/flutter_blurhash.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:google_fonts/google_fonts.dart';
 import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
+import 'package:octo_image/octo_image.dart';
 import 'package:provider/provider.dart';
 import 'home_uitgaantabel_kaart_component_model.dart';
 export 'home_uitgaantabel_kaart_component_model.dart';
@@ -249,15 +251,21 @@ class _HomeUitgaantabelKaartComponentWidgetState
                                         return ClipRRect(
                                           borderRadius:
                                               BorderRadius.circular(8.0),
-                                          child: CachedNetworkImage(
-                                            fadeInDuration:
-                                                Duration(milliseconds: 500),
-                                            fadeOutDuration:
-                                                Duration(milliseconds: 500),
-                                            imageUrl: getJsonField(
-                                              evenementenItem,
-                                              r'''$.logo''',
-                                            ).toString(),
+                                          child: OctoImage(
+                                            placeholderBuilder: (_) =>
+                                                SizedBox.expand(
+                                              child: Image(
+                                                image: BlurHashImage(
+                                                    'L6PZfSi_.AyE_3t7t7R**0o#DgR4'),
+                                                fit: BoxFit.cover,
+                                              ),
+                                            ),
+                                            image: CachedNetworkImageProvider(
+                                              getJsonField(
+                                                evenementenItem,
+                                                r'''$.logo''',
+                                              ).toString(),
+                                            ),
                                             fit: BoxFit.fitHeight,
                                           ),
                                         );
@@ -551,6 +559,7 @@ class _HomeUitgaantabelKaartComponentWidgetState
                                               evenementenItem,
                                               r'''$.plaats''',
                                             ).toString(),
+                                            maxLines: 1,
                                             style: FlutterFlowTheme.of(context)
                                                 .bodySmall
                                                 .override(

+ 25 - 13
lib/uitgaanspaginas/p_uitgaan_slider_kaart_component/p_uitgaan_slider_kaart_component_widget.dart

@@ -139,19 +139,31 @@ class _PUitgaanSliderKaartComponentWidgetState
                     children: [
                       Align(
                         alignment: AlignmentDirectional(1.0, 0.0),
-                        child: Padding(
-                          padding: EdgeInsetsDirectional.fromSTEB(
-                              0.0, 10.0, 0.0, 10.0),
-                          child: ClipRRect(
-                            borderRadius: BorderRadius.circular(8.0),
-                            child: Image.network(
-                              widget!.logo!,
-                              width: double.infinity,
-                              height: double.infinity,
-                              fit: BoxFit.cover,
-                              alignment: Alignment(0.0, 0.0),
-                            ),
-                          ),
+                        child: Builder(
+                          builder: (context) {
+                            if (widget!.logo != '') {
+                              return Padding(
+                                padding: EdgeInsetsDirectional.fromSTEB(
+                                    0.0, 10.0, 0.0, 10.0),
+                                child: ClipRRect(
+                                  borderRadius: BorderRadius.circular(8.0),
+                                  child: Image.network(
+                                    widget!.logo!,
+                                    width: double.infinity,
+                                    height: double.infinity,
+                                    fit: BoxFit.cover,
+                                    alignment: Alignment(0.0, 0.0),
+                                  ),
+                                ),
+                              );
+                            } else {
+                              return Icon(
+                                Icons.image_not_supported,
+                                color: FlutterFlowTheme.of(context).primaryText,
+                                size: 24.0,
+                              );
+                            }
+                          },
                         ),
                       ),
                       Align(

+ 9 - 1
pubspec.lock

@@ -320,6 +320,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.5.0"
+  flutter_blurhash:
+    dependency: "direct main"
+    description:
+      name: flutter_blurhash
+      sha256: e97b9aff13b9930bbaa74d0d899fec76e3f320aba3190322dcc5d32104e3d25d
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.1"
   flutter_cache_manager:
     dependency: "direct main"
     description:
@@ -778,7 +786,7 @@ packages:
     source: hosted
     version: "1.0.0"
   octo_image:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: octo_image
       sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"

+ 2 - 0
pubspec.yaml

@@ -50,6 +50,7 @@ dependencies:
   fetch_api: 2.2.0
   fetch_client: 1.1.2
   flutter_animate: 4.5.0
+  flutter_blurhash: 0.9.1
   flutter_cache_manager: 3.4.1
   flutter_html:
   flutter_secure_storage: 10.0.0-beta.4
@@ -71,6 +72,7 @@ dependencies:
   intl: 0.20.2
   json_path: 0.7.2
   mime_type: 1.0.0
+  octo_image: 2.1.0
   page_transition: 2.1.0
   path_provider: 2.1.4
   path_provider_android: 2.2.10