Sfoglia il codice sorgente

Fix EvenementHorecagelegenheid image-crash (P1); restructure TASKS.md with stable IDs + concurrency guard; sync FlutterFlow state

- Completed the CachedNetworkImage empty-URL crash fix on
  EvenementHorecagelegenheid's Carousel image: built the ConditionalBuilder
  condition from scratch (fotoos item Is Set) and added the 'image not
  supported' else-icon. This also happened to be blocking ALL FlutterFlow
  exports (2 project errors: unset condition + Else branch with no child) -
  unrelated to this session's own work, found via the Issues panel per
  CLAUDE.md convention.
- TASKS.md: switched to stable per-task IDs (P0-1, P1-1, ...) + explicit
  'Eigenaar:' line, and added a concurrency-collision warning/convention -
  two parallel Claude Code sessions independently picked up the same P0
  task today (Bob had to referee), so tasks now need an explicit '- bezig'
  claim before starting.
- CLAUDE.md: documented the same concurrency convention.
- Home-slider (P0-1): Bob's 'Show Empty List Widget' toggle doesn't appear
  to have landed yet after a fresh export + builder re-check - flagged for
  him to reconfirm, not marked done.
- Synced latest FlutterFlow export (Bob's own concurrent custom_code/
  actions work + platform/gradle bumps included).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bob 1 mese fa
parent
commit
9eaab628b8

+ 18 - 8
CLAUDE.md

@@ -16,7 +16,7 @@ zoeken of scannen — scope alle bestandsoperaties tot dit project.
 - **Groepeer opgepakte taken per sessie op FlutterFlow-paginagebied**
 - **Groepeer opgepakte taken per sessie op FlutterFlow-paginagebied**
   waar mogelijk (bv. twee taken op dezelfde pagina/component samen
   waar mogelijk (bv. twee taken op dezelfde pagina/component samen
   oppakken) — minder heen-en-weer-navigeren in de builder, minder
   oppakken) — minder heen-en-weer-navigeren in de builder, minder
-  tokens. Zie de sessie-indeling bovenaan `TASKS.md`.
+  tokens.
 - **`TASKS.md`-status kan achterlopen op de echte code** (Bob werkt
 - **`TASKS.md`-status kan achterlopen op de echte code** (Bob werkt
   gelijktijdig, en documentatie-updates lopen niet altijd synchroon
   gelijktijdig, en documentatie-updates lopen niet altijd synchroon
   met de export). Check een taak die er al even staat met
   met de export). Check een taak die er al even staat met
@@ -24,13 +24,23 @@ zoeken of scannen — scope alle bestandsoperaties tot dit project.
   `grep` vóórdat je 'm oppakt — niet blind vertrouwen dat "open"
   `grep` vóórdat je 'm oppakt — niet blind vertrouwen dat "open"
   betekent "nog niet gefixt". (Precedent: 2026-08-04 bleken 2 "open"
   betekent "nog niet gefixt". (Precedent: 2026-08-04 bleken 2 "open"
   P0-taken al op 2026-08-02 gefixt te zijn, git-bevestigd.)
   P0-taken al op 2026-08-02 gefixt te zijn, git-bevestigd.)
-- **Eigenaar-tag per taak in `TASKS.md`**: `[Claude]` (zelfstandig of
-  via browser-automation te doen), `[Bob]` (sneller/simpeler voor Bob
-  zelf — meestal builder-UI met een bekend fragiele dialoog, zie
-  hieronder), of `[Bob — bezig]` (al geclaimd). Vuistregel: een kort,
-  mechanisch herhaald patroon zonder geneste dialogen → Claude;
-  ConditionalBuilder/JSON-Path-condities, List-typed
-  function-argumenten, of iets dat eerder al vastliep → Bob.
+- **Elke taak in `TASKS.md` heeft een stabiel ID (bv. `P0-1`) en een
+  `Eigenaar:`-regel** — `Bob` (sneller/simpeler voor hem zelf, meestal
+  builder-UI met een bekend fragiele dialoog, zie hieronder), `Claude`
+  (onbeklaimd, vrij op te pakken), of **`... — bezig`** (iemand is er
+  *nu* actief mee bezig). Vuistregel voor wie een nieuwe taak zou
+  moeten doen: een kort, mechanisch herhaald patroon zonder geneste
+  dialogen → Claude; ConditionalBuilder/JSON-Path-condities,
+  List-typed function-argumenten, of iets dat eerder al vastliep →
+  Bob.
+- **⚠️ Concurrency: Bob start elke taak in een nieuwe chat, dus er
+  kunnen meerdere sessies tegelijk actief zijn.** Check vóór je een
+  taak oppakt of de Eigenaar-regel al "— bezig" zegt door iemand
+  anders — zo ja, niet zelfstandig ook gaan bouwen aan hetzelfde
+  bestand/component, vraag Bob eerst wie 'm afmaakt. Zet zelf "—
+  bezig" zodra je serieus begint. (Precedent 2026-08-04: twee sessies
+  pakten onafhankelijk dezelfde P0-taak op — Bob moest scheidsrechteren
+  tussen twee stukken werk aan hetzelfde bestand.)
 
 
 ## Sessiegeheugen — afsluitroutine
 ## Sessiegeheugen — afsluitroutine
 
 

+ 223 - 216
TASKS.md

@@ -2,231 +2,238 @@
 
 
 Bijgewerkt: 2026-08-04. Zie `CLAUDE.md` voor werkinstructies/conventies.
 Bijgewerkt: 2026-08-04. Zie `CLAUDE.md` voor werkinstructies/conventies.
 Elke openstaande taak hieronder is zelfstandig te begrijpen zonder de
 Elke openstaande taak hieronder is zelfstandig te begrijpen zonder de
-chat gelezen te hebben waarin hij ontstond. Volgorde binnen elke
-prioriteit = geschatte ernst/impact, hoogste eerst. Eigenaar-tag per
-taak: **[Claude]** (zelfstandig/via browser-automation te doen),
-**[Bob]** (sneller/simpeler voor hem zelf — meestal builder-UI met een
-bekend fragiele dialoog, zie `CLAUDE.md`), **[Bob — bezig]** (al
-geclaimd).
-
-**2026-08-04 documentatie-correctie:** 2 taken die hier als open
-stonden bleken al gefixt (git-bevestigd, commits van 2026-08-02) —
-verwijderd: "Hello Worlddd"-debugtekst en de Event→Horeca
-overflow-bug. Ook bleek er **nergens in de app een werkend
-`hasError`-patroon** te bestaan om te kopiëren (zie `CLAUDE.md`,
-Domein/architectuurcontext) — de foutafhandelings-taken hieronder gaan
-dus uit van een nieuw te ontwerpen patroon, niet een bestaand te
-repliceren patroon.
-
-## Sessie-indeling (suggestie voor tokenefficiëntie)
-
-Groepeer bij het oppakken bij voorkeur per FlutterFlow-paginagebied:
-- **Nu opgepakt:** Home-slider empty-state (bleek uiteindelijk
-  [Bob, 10 sec] — zie P0 #1) + Foutafhandeling Horeca-overzicht
-  ([Claude], nog te onderzoeken — zie P0 #2).
-- **Volgende sessie, Login/favorieten-afronding:** lege-lijst
-  empty-state favorietenpagina + hartje-op-gemeente (ná designantwoord)
-  + favorieten-sync-verificatie.
-- **Volgende sessie, laatste image-crash + Flexible-wrap:**
-  EvenementHorecagelegenheid-carousel [Bob] + sliderkaartje-datumregel
-  [Claude] — klein, kan samen met opruimwerk in dezelfde pagina's.
-- **Volgende sessie, toegankelijkheid:** alle 5 punten onder
-  "Toegankelijkheid & navigatie" samen — klein en gelijksoortig.
-- **Volgende sessie, opschonen:** lib/kanweg + dubbel menu + EventWidget-route.
+chat gelezen te hebben waarin hij ontstond.
+
+**Formaat van elke taak:** `**P0-1 · Eigenaar: ...**` als eerste regel
+— een stabiel nummer (verandert niet als andere taken worden
+afgerond/verwijderd) + wie 'm oppakt. Volgorde binnen elke prioriteit
+= geschatte ernst/impact, hoogste eerst.
+
+**Eigenaar-waarden:**
+- **Bob** — sneller/simpeler voor hem zelf (meestal builder-UI met een
+  bekend fragiele dialoog, zie `CLAUDE.md`).
+- **Claude** — zelfstandig/via browser-automation te doen, nog
+  onbeklaimd.
+- **Claude — bezig (sessie <kenmerk>)** / **Bob — bezig** — een
+  sessie/persoon is hier *nu* actief mee bezig.
+- **Onbepaald** — nog geen eigenaar gekozen.
+
+**⚠️ Voorkom dubbel werk:** Bob start elke taak in een nieuwe, aparte
+chat — er kunnen dus meerdere sessies tegelijk actief zijn op
+hetzelfde project. **Voordat je een taak oppakt: check of de
+Eigenaar-regel al "— bezig" zegt.** Zo ja: niet zelfstandig ook gaan
+zitten werken aan hetzelfde bestand/component — vraag Bob eerst wie
+'m afmaakt (dit gebeurde op 2026-08-04: twee sessies pakten
+onafhankelijk P0-2 op; Bob moest scheidsrechteren). Zet zelf "— bezig"
+in de Eigenaar-regel zodra je serieus aan een taak begint, en haal het
+weer weg (of verwijder de taak, zie hieronder) zodra je stopt.
+
+**Afronden:** een volledig afgeronde taak wordt **verwijderd** uit
+deze lijst (niet gearchiveerd). Bij gedeeltelijke voortgang: taak laten
+staan met bijgewerkte inhoud die alleen het resterende werk beschrijft.
 
 
 ## Status: actief bij Bob (geen Claude-actie nodig, ter info)
 ## Status: actief bij Bob (geen Claude-actie nodig, ter info)
 
 
-- **FavorietenAgenda 403-fout** [Bob — bezig]: Bob troubleshoot dit
-  zelf. FlutterFlow-requests komen bij nginx binnen met een lege
-  Cookie-header; zijn eigen browser/curl (via dezelfde Cloudflare-edge)
-  sturen de sessie-cookie wél mee. Cloudflare Cache Rule voor `SSESS*`
-  staat al goed (bypass, eerste regel). Werkhypothese: Cloudflare Bot
-  Management behandelt FlutterFlow's server-side traffic anders op
-  basis van IP-reputatie/TLS-fingerprint. Niet opnieuw diagnosticeren
-  zonder navraag — check eerst of Bob al verder is.
+- **FavorietenAgenda 403-fout** — **Eigenaar: Bob — bezig.** Bob
+  troubleshoot dit zelf. FlutterFlow-requests komen bij nginx binnen
+  met een lege Cookie-header; zijn eigen browser/curl (via dezelfde
+  Cloudflare-edge) sturen de sessie-cookie wél mee. Cloudflare Cache
+  Rule voor `SSESS*` staat al goed (bypass, eerste regel).
+  Werkhypothese: Cloudflare Bot Management behandelt FlutterFlow's
+  server-side traffic anders op basis van IP-reputatie/
+  TLS-fingerprint. Niet opnieuw diagnosticeren zonder navraag — check
+  eerst of Bob al verder is.
 
 
 ## P0 — blokkeert livegang
 ## P0 — blokkeert livegang
 
 
-1. [ ] **Home-slider crasht (RangeError) bij 0 API-resultaten — fix
-   gelokaliseerd, 1 klik voor Bob nodig.** [Bob, 10 seconden werk]
-   Bob koos: bij 0 resultaten component overslaan (niets tonen), geen
-   melding. `CarouselSlider.builder` met `itemCount: 0` crasht. Commit
-   `1a22bbf` (2026-08-03, Bob) wrapte de Home-slider al in een
-   ConditionalBuilder met een altijd-waar conditie — bleek bij
-   onderzoek 2026-08-04 op het verkeerde niveau te zitten (buiten de
-   FutureBuilder/data-scope, kan de resultatenlijst dus niet zien) en
-   is dus geen echte fix. **Betere, native route gevonden:** de
-   `Carousel`-widget zelf (child van If → Container in de tree) heeft
-   een ingebouwde **"Empty List Widget"**-property — precies bedoeld
-   voor dit scenario, geen ConditionalBuilder nodig. Stappen:
-   FlutterFlow → component `HomeUitgaanSliderComponent` → Widget Tree
-   → selecteer de `Carousel`-node (onder ConditionalBuilder → If →
-   Container) → rechterpaneel → **"Empty List Widget"** → vink **"Show
-   Empty List Widget"** aan → als widget een lege/onzichtbare placeholder
-   instellen (matcht "component overslaan"). **Bij Bob i.p.v. Claude:**
-   die checkbox rendert buiten de klikbare/screenshot-ruimte (bevestigd
-   na 5 pogingen, zie CLAUDE.md "Rechterpaneel te breed") — ook na het
-   vergroten van het venster. Kost Bob ~10 seconden, kostte mij te veel
-   tool-calls voor niks.
-
-2. [ ] **Foutafhandeling ontbreekt op HorecagelegenhedenOverzicht.**
-   [Claude, nog te onderzoeken] Bob's antwoord: gewoon skippen zodra er
-   iets misgaat, geen melding/retry-knop nodig — "als de app maar niet
-   crasht, 0 inhoud zou een outage betekenen". 6 FutureBuilders checken
-   alleen `!snapshot.hasData` — bij een mislukte call blijft het
-   laadwieltje voor altijd draaien (geen crash, wel een dooie boel).
-   Nog niet onderzocht of hier ook een native "empty/error state"-optie
-   bestaat zoals bij de Carousel hierboven, of dat dit alsnog handmatige
-   conditie-opbouw vereist (en dus mogelijk ook de rechterpaneel-klik-
-   beperking raakt). Oppakken in de volgende sessie.
-   `lib/horecagelegenhedenoverzicht/horecagelegenheden_overzicht/horecagelegenheden_overzicht_widget.dart`
-   (regels 283, 467, 651, 835, 1019, 1203).
-   **Vraag aan Bob vóór starten:** akkoord met "compacte melding +
-   Opnieuw proberen-knop" als aanpak, of andere voorkeur voor
-   tekst/vormgeving?
-
-3. [ ] **Hartje-icoon op gemeente — geblokkeerd op designvraag.** [Bob]
-   Bob koos: heart ergens anders dan de gemeente-dropdown zelf (die
-   blijft ongewijzigd). Blocker: **geen enkele pagina toont de
-   geselecteerde gemeente-náám als tekst** — alleen het numerieke ID
-   (`FFAppState().gemeenteSelectId`) wordt doorgegeven als filter (bv.
-   `plaats`/`townid` param op `PUitgaanPageWidget`). `GemeentenCall.
-   gemeenteName()` in `lib/backend/api_requests/api_calls.dart` bestaat
-   al maar is ongebruikt. **Vraag aan Bob:** waar moeten gemeente-naam +
-   hartje komen (kandidaat: AppBar van `PUitgaanPageWidget`)? Niet zelf
-   raden aan de layout.
-   - Bekende valkuil als dit gebouwd wordt: bind de naam niet live via
-     een `List<CustomDataType>`-typed lookup function-argument (bv.
-     `provincieNaamById`/`gemeenteNaamById` in
-     `lib/flutter_flow/custom_functions.dart`, al gebouwd, compileert,
-     nog niet gekoppeld) — dat argument-bindingsdialoog loopt vast op
-     de geneste Set Variable-bug (zie `CLAUDE.md`). Simpeler: naam
-     vastleggen in een plain String App State-variabele op moment van
-     selectie i.p.v. 'm live op te zoeken.
-
-4. [ ] **Werkende login + favorieten — resterende deeltjes.** [Bob]
-   Basis is af: login gekoppeld, wachtwoord-vergeten-flow, favorieten
-   drawer-link + 3-tabblad-pagina, hartje op horeca-kaart +
-   -detailpagina. Nog open:
-   - Lege-lijst empty-state op de favorietenpagina (nu kale/lege
-     Container als er nog geen favorieten zijn). `lib/favorieten/favorieten_widget.dart`.
-   - Bevestigen dat favorieten écht syncen via Drupal bij inloggen —
-     hangt af van het FavorietenAgenda-403-issue hierboven.
-
-5. [ ] **Drupal: anonieme leestoegang onderzoeken voor browse-
-   endpoints.** [Bob] Drupal-niveau, geen Claude-taak.
+**P0-1 · Eigenaar: Bob (10 sec klusje) — status onbevestigd, zie
+onder.** Home-slider crasht (RangeError) bij 0 API-resultaten. Bob
+koos: bij 0 resultaten component overslaan (niets tonen), geen
+melding. `CarouselSlider.builder` met `itemCount: 0` crasht. Native
+fix gevonden: `Carousel`-widget (component `HomeUitgaanSliderComponent`
+→ Widget Tree → ConditionalBuilder → If → Container → **Carousel**) →
+rechterpaneel → **"Empty List Widget"** → vink **"Show Empty List
+Widget"** aan → widget op een lege/onzichtbare placeholder zetten.
+**2026-08-04, na een verse export + builder-check: nog geen wijziging
+zichtbaar** — noch in de geëxporteerde code, noch in het "Empty List
+Widget"-paneel zelf (geen "Show"-checkbox aangevinkt te zien). Mogelijk
+is de klik niet aangekomen (zelfde rechterpaneel-clipping-risico als
+waarom Claude 'm niet zelf kon doen, zie `CLAUDE.md`). **Bob: kun je
+nogmaals checken of de checkbox echt aanstaat?**
+
+**P0-2 · Eigenaar: andere Claude-sessie — bezig.** Foutafhandeling
+ontbreekt op HorecagelegenhedenOverzicht. **Niet oppakken vanuit deze
+sessie of een nieuwe sessie zonder eerst bij Bob te checken of de
+andere sessie nog bezig is.** Bevinding van die sessie (accurater dan
+eerdere aanname hier): het is geen "spinner blijft draaien"-bug maar
+een **crash** — bij een mislukte API-call (`statusCode: -1, jsonBody:
+null`) doet de code `getJsonField(null, '$').toList()` →
+crash, op 6 identieke plekken in
+`lib/horecagelegenhedenoverzicht/horecagelegenheden_overzicht/horecagelegenheden_overzicht_widget.dart`
+(regels 327/511/695/879/1063/1247, telkens direct na de
+`!snapshot.hasData`-check). Bob's antwoord blijft gelden: gewoon
+skippen zodra er iets misgaat, geen melding/retry-knop nodig.
+
+**P0-3 · Eigenaar: Bob — geblokkeerd op designvraag.** Hartje-icoon op
+gemeente. Bob koos: heart ergens anders dan de gemeente-dropdown zelf
+(die blijft ongewijzigd). **2026-08-04, Bob's antwoord:** hartje naast
+de provincienaam én naast de gemeentenaam, in de titel. Bob wil dat
+**elke pagina in de header de provincie- of gemeentenaam toont**, zodat
+gebruikers altijd zien waar ze zitten. **Nog te doen: Claude stelt een
+concreet voorstel op voor hoe dit in de gedeelde header verwerkt wordt**
+(welke pagina's, welke plek exact, hoe de naam ophalen — zie de
+bekende valkuil hieronder) en legt dat voor aan Bob vóór het bouwen.
+- Blocker die dit voorstel moet oplossen: **geen enkele pagina toont de
+  geselecteerde gemeente-náám als tekst** — alleen het numerieke ID
+  (`FFAppState().gemeenteSelectId`) wordt doorgegeven als filter (bv.
+  `plaats`/`townid` param op `PUitgaanPageWidget`). `GemeentenCall.
+  gemeenteName()` in `lib/backend/api_requests/api_calls.dart` bestaat
+  al maar is ongebruikt.
+- Bekende valkuil als dit gebouwd wordt: bind de naam niet live via
+  een `List<CustomDataType>`-typed lookup function-argument (bv.
+  `provincieNaamById`/`gemeenteNaamById` in
+  `lib/flutter_flow/custom_functions.dart`, al gebouwd, compileert,
+  nog niet gekoppeld) — dat argument-bindingsdialoog loopt vast op de
+  geneste Set Variable-bug (zie `CLAUDE.md`). Simpeler: naam
+  vastleggen in een plain String App State-variabele op moment van
+  selectie i.p.v. 'm live op te zoeken.
+
+**P0-4 · Eigenaar: Bob.** Werkende login + favorieten — resterende
+deeltjes. Basis is af: login gekoppeld, wachtwoord-vergeten-flow,
+favorieten drawer-link + 3-tabblad-pagina, hartje op horeca-kaart +
+-detailpagina. Nog open:
+- Lege-lijst empty-state op de favorietenpagina (nu kale/lege
+  Container als er nog geen favorieten zijn).
+  `lib/favorieten/favorieten_widget.dart`.
+- Bevestigen dat favorieten écht syncen via Drupal bij inloggen —
+  hangt af van FavorietenAgenda-403 hierboven.
+
+**P0-5 · Eigenaar: Bob.** Drupal: anonieme leestoegang onderzoeken
+voor browse-endpoints. Drupal-niveau, geen Claude-taak.
 
 
 ## P1 — snel na livegang
 ## P1 — snel na livegang
 
 
-- [ ] **Lege image-URL crash (CachedNetworkImage) — 1 van 10
-  resterend.** [Bob] Patroon in `CLAUDE.md` ("Patroon: lege/ontbrekende
-  afbeeldings-URL"). 9 van de 10 bekende instanties zijn al gefixt en
-  gepusht. **Laatste resterende, status gecorrigeerd 2026-08-04:**
-  component `EvenementHorecagelegenheid`, Carousel-Image
-  (`imageUrl: fotoosItem`, regel ~299) — er staat **nog geen**
-  ConditionalBuilder-wrap (eerdere aanname dat de wrap al stond en
-  alleen de conditie `Unset` was, klopt niet met de huidige export).
-  Moet van scratch: wrap + conditie ("Is Set") + else-icon, zoals
-  omschreven in `CLAUDE.md`. Dit is exact het patroon dat eerder al
-  meerdere keren vastliep op de geneste Set Variable-dialoog — vandaar
-  Bob i.p.v. Claude.
-  - Nog niet herchecked op nieuwe pagina's sinds de laatste gerichte
-    `grep -rl "CachedNetworkImage"` over `lib/**/*_widget.dart`.
-
-- [ ] **Loading/foutafhandeling-patroon uitrollen naar overige
-  lijst-/detailpagina's.** [Claude, ná P0 #2] Zodra het patroon uit P0
-  #2 (Horeca-overzicht) staat, hetzelfde toepassen op Event,
-  HorecagelegenheidCurrent, sliders — nu nergens een echte
-  foutstatus, alleen "geen data nog".
-
-- [ ] **Overflow op Home-pagina.** [Claude] Live gereproduceerd tijdens
-  testen (2026-08-03): `RenderFlex overflowed by 7338 pixels` in een
-  `Column` zonder `Expanded`/scrollcontainer.
-  `lib/uitgaanspaginas/home/home_widget.dart:316`.
-
-- [ ] **Sliderkaartje: datumregel mist de Flexible-wrap.** [Claude]
-  Zelfde mechanische patroon als de twee regels eronder die al gefixt
-  zijn (consistentie) — bevestigd nog open.
-  `p_uitgaan_slider_kaart_component_widget.dart` (Row met
-  `Icons.calendar_month` + Text zonder Flexible).
-
-- [ ] **EstablishmentsCall crasht zonder categoriefilter.** [Bob]
-  `horcat ??= null!;` — bevestigd nog aanwezig, regel 1443. Nu geen
-  probleem omdat elke aanroep toevallig altijd een filter meegeeft,
-  wel een landmijn voor de toekomst. `lib/backend/api_requests/api_calls.dart`.
-  Fix zit vermoedelijk in de FlutterFlow API-call-configuratie (default
-  parameterwaarde), niet in lokale code.
-
-- [ ] **"Thuis bezorgen" koppelen aan een echt leverbaar-veld per
-  horecagelegenheid.** [Bob] Wacht op Bob: eerst het API-endpoint aan
-  Drupal-kant configureren. Daarna tonen/verbergen op basis van het
-  echte veld i.p.v. de huidige dode tap.
-
-- [ ] **~25+ plekken tonen letterlijke veldnaam i.p.v. nette
-  placeholder** (bv. "titel", "adres", "def") bij ontbrekende data.
-  [Claude] Nog niet geïnventariseerd welke exact — audit nog te doen.
-
-- [ ] **Account/profielscherm** (wachtwoord wijzigen, uitloggen,
-  account verwijderen). [Bob] Bovenop de login/favorieten-P0-basis.
-  Account verwijderen heeft mogelijk AVG-implicaties aan Drupal-kant.
-
-- [ ] **Toegankelijkheid & navigatie, restpunten:** [Claude, klein
-  batchbaar in 1 sessie]
-  - Login-pagina: 2 icon-only knoppen zonder tooltip (veld wissen +
-    wachtwoord tonen/verbergen) — `login_widget.dart` (suffixIcon
-    van een `TextFormField` — standaard Tooltip-wrap werkt daar niet
-    op, zie `CLAUDE.md`; bevestigd nog open).
-  - Overige icon-only knoppen buiten de header van tooltip voorzien.
-  - Bevestig-stap toevoegen aan de provincie/gemeente-kiezer (nu geen
-    "Toepassen"-knop of bevestiging).
-  - Vaste "terug naar hoofdlijst"-snelkoppeling op detailpagina's.
-  - Deep-link/query-param robuustheid checken (ongeldig/verlopen
-    `nid`).
-
-- [ ] **Visuele polish** (los, per pagina): [Bob/Claude, geen
-  builder-risico's bekend] Uitgaan-pagina (typografie/contrast/spacing),
-  Event-pagina, HorecagelegenhedenOverzicht, tabel-kaartje-afstemming
-  met slider-kaartje, categorie-tag mag titel/datum niet overstemmen.
-
-- [ ] **Performance:** [onderzoek nodig, geen owner bepaald]
-  - Dubbele/onnodige netwerkverzoeken checken (audit, nog te doen).
-  - 6-7 tabbladen laden allemaal meteen hun eigen data bij elke
-    tabwissel/herbouw (TabBarView bouwt alle tabs eager) —
-    `horecagelegenheden_overzicht(_sort_page/_page_data_type)_widget.dart`.
-
-- [ ] **Responsive/screensize-audit** [onderzoek nodig] — o.a. het
-  vaste 200×200px-tegelformaat in het venue-event-grid dat botst met de
-  grid-berekening.
+**P1-1 · Eigenaar: Claude, ná P0-2.** Loading/foutafhandeling-patroon
+uitrollen naar overige lijst-/detailpagina's. Zodra het patroon uit
+P0-2 (Horeca-overzicht) staat, hetzelfde toepassen op Event,
+HorecagelegenheidCurrent, sliders — nu nergens een echte foutstatus,
+alleen "geen data nog".
+
+**P1-2 · Eigenaar: Claude.** Overflow op Home-pagina. Live
+gereproduceerd tijdens testen (2026-08-03): `RenderFlex overflowed by
+7338 pixels` in een `Column` zonder `Expanded`/scrollcontainer.
+`lib/uitgaanspaginas/home/home_widget.dart:316`.
+
+**P1-3 · Eigenaar: Claude.** Sliderkaartje: datumregel mist de
+Flexible-wrap. Zelfde mechanische patroon als de twee regels eronder
+die al gefixt zijn (consistentie) — bevestigd nog open.
+`p_uitgaan_slider_kaart_component_widget.dart` (Row met
+`Icons.calendar_month` + Text zonder Flexible).
+
+**P1-4 · Eigenaar: Bob.** EstablishmentsCall crasht zonder
+categoriefilter. `horcat ??= null!;` — bevestigd nog aanwezig, regel
+1443. Nu geen probleem omdat elke aanroep toevallig altijd een filter
+meegeeft, wel een landmijn voor de toekomst.
+`lib/backend/api_requests/api_calls.dart`. Fix zit vermoedelijk in de
+FlutterFlow API-call-configuratie (default parameterwaarde), niet in
+lokale code.
+
+**P1-5 · Eigenaar: Bob.** "Thuis bezorgen" koppelen aan een echt
+leverbaar-veld per horecagelegenheid. Wacht op Bob: eerst het
+API-endpoint aan Drupal-kant configureren. Daarna tonen/verbergen op
+basis van het echte veld i.p.v. de huidige dode tap.
+
+**P1-6 · Eigenaar: Bob.** Letterlijke veldnaam i.p.v. nette placeholder
+bij ontbrekende data — audit afgerond 2026-08-04, 13 treffers. Patroon:
+`valueOrDefault<String>(<bron>, '<default>')` waarbij de default gelijk
+is aan de veld-/functienaam. Fix is mechanisch (Default Value-tekst
+aanpassen in de builder op het Text-widget), maar raakt waarschijnlijk
+dezelfde "Set from Variable"-property als de Empty-URL-fix — zelfde
+risico-inschatting als het CachedNetworkImage-patroon.
+- `horecagelegenheidoverzicht_kaart_widget.dart:357/381/408` — titel/adres/plaats
+- `event_current_widget.dart:416` — nid
+- `evenement_info_widget.dart:134/156/211/236/291` — adres/plaats/entreeprijs/entreetoelichting/contact
+- `evenement_component_widget.dart:216/238` — title/eventDate
+- Grensgevallen (geen letterlijke veldnaam maar wel technische/zinloze
+  placeholder, zelfde fix-aanpak): `p_uitgaan_slider_kaart_component_widget.dart:284`
+  toont `'def'` als datum ontbreekt; `evenement_horecagelegenheid_widget.dart:406`
+  toont `'establishmentnid'`.
+- Niet-verdachte gevallen bewust genegeerd (echte inhoudelijke
+  fallback, geen technische naam): `'Evenement'`
+  (`kaart_tabel_uitgaan_comp_widget.dart:214`,
+  `kaart_tabel_uitgaan_s_comp_widget.dart:121`) en `'Uitgaan'`
+  (`tag_categorie_component_widget.dart:111`).
+
+**P1-7 · Eigenaar: Bob.** Account/profielscherm (wachtwoord wijzigen,
+uitloggen, account verwijderen). Bovenop de login/favorieten-P0-basis.
+Account verwijderen heeft mogelijk AVG-implicaties aan Drupal-kant.
+
+**P1-8 · Eigenaar: Claude, klein batchbaar in 1 sessie.**
+Toegankelijkheid & navigatie, restpunten:
+- Login-pagina: 2 icon-only knoppen zonder tooltip (veld wissen +
+  wachtwoord tonen/verbergen) — `login_widget.dart` (suffixIcon van
+  een `TextFormField` — standaard Tooltip-wrap werkt daar niet op, zie
+  `CLAUDE.md`; bevestigd nog open).
+- Overige icon-only knoppen buiten de header van tooltip voorzien.
+- Bevestig-stap toevoegen aan de provincie/gemeente-kiezer (nu geen
+  "Toepassen"-knop of bevestiging).
+- Vaste "terug naar hoofdlijst"-snelkoppeling op detailpagina's.
+- Deep-link/query-param robuustheid checken (ongeldig/verlopen `nid`).
+
+**P1-9 · Eigenaar: Onbepaald.** Visuele polish (los, per pagina):
+Uitgaan-pagina (typografie/contrast/spacing), Event-pagina,
+HorecagelegenhedenOverzicht, tabel-kaartje-afstemming met
+slider-kaartje, categorie-tag mag titel/datum niet overstemmen.
+
+**P1-10 · Eigenaar: Onbepaald, onderzoek nodig.** Performance:
+- Dubbele/onnodige netwerkverzoeken checken (audit, nog te doen).
+- 6-7 tabbladen laden allemaal meteen hun eigen data bij elke
+  tabwissel/herbouw (TabBarView bouwt alle tabs eager) —
+  `horecagelegenheden_overzicht(_sort_page/_page_data_type)_widget.dart`.
+
+**P1-11 · Eigenaar: Onbepaald, onderzoek nodig.**
+Responsive/screensize-audit — o.a. het vaste 200×200px-tegelformaat in
+het venue-event-grid dat botst met de grid-berekening.
 
 
 ## P2 — features & concept, na livegang
 ## P2 — features & concept, na livegang
 
 
-- [ ] **Datum-filter**: Vandaag / Dit weekend / Deze week.
-- [ ] **Google Maps-weergave** van horecagelegenheden — bewust niet
-  vóór livegang (Maps API-key + billing, onduidelijk of elke locatie
-  al lat/long heeft, marker-UI — geen quick win).
-- [ ] **"In de buurt"/geolocatie-browsen** — aanvulling op het
-  provincie/gemeente-model, geen vervanging.
-- [ ] "Wat is er vanavond"-melding/widget.
-- [ ] Deel-knop op eventpagina.
-- [ ] "Toevoegen aan agenda" (native kalender).
-- [ ] "Events op deze locatie" prominenter op de horeca-detailpagina.
-- [ ] Reviews/waardering voor horecagelegenheden — groot, vraagt nieuw
-  Drupal content-type + moderatie, eigen project.
-- [ ] Ad-banners op horeca-overzicht, horeca-detail, event-detail;
-  vaste regel: geen ads op locatie-kiezer/login/account.
-- [ ] Tekstzoeken op titel op de horeca-overzichtspagina (los van de
-  Datatype/Sort-experimenten die Bob daar zelf op test — die zijn
-  actief werk-in-uitvoering, geen dode code).
-- [ ] **Opschonen:** [Bob]
-  - Merge `kaartTabelUitgaanComp` + `kaartTabelUitgaanSComp` — Bob
-    doet dit zelf ("ik kijk er zelf naar").
-  - `lib/kanweg` opnieuw leegmaken indien teruggekomen na een latere
-    export-pull, plus eventuele nieuwe losse dode componenten in
-    `lib/evenement/`.
-  - Eén browse-by-category-patroon i.p.v. twee: nu Home landelijk is,
-    bepalen hoe Home's categorieën en `PUitgaanPage`'s provincie/
-    gemeente-gescoopte categorieën zich tot elkaar verhouden.
-  - Dubbele Provincie/Gemeente-blok in het menu opruimen (12
-    menu-items waar 6 zouden volstaan).
-  - `EventWidget`-route: heraansluiten of definitief schrappen (losse
-    orphan-route).
+**P2-1 · Eigenaar: Bob — testen in de API, komt terug.** Datum-filter:
+Vandaag / Dit weekend / Deze week.
+
+**P2-2 · Eigenaar: Onbepaald.** Google Maps-weergave van
+horecagelegenheden — bewust niet vóór livegang (Maps API-key +
+billing, onduidelijk of elke locatie al lat/long heeft, marker-UI —
+geen quick win).
+
+**P2-3 · Eigenaar: Onbepaald.** "In de buurt"/geolocatie-browsen —
+aanvulling op het provincie/gemeente-model, geen vervanging.
+
+**P2-4 · Eigenaar: Onbepaald.** Overige feature-ideeën, geen van alle
+uitgewerkt: "Wat is er vanavond"-melding/widget · deel-knop op
+eventpagina · "toevoegen aan agenda" (native kalender) · "events op
+deze locatie" prominenter op de horeca-detailpagina · reviews/
+waardering voor horecagelegenheden (groot, vraagt nieuw Drupal
+content-type + moderatie, eigen project).
+
+**P2-5 · Eigenaar: Onbepaald.** Ad-banners op horeca-overzicht,
+horeca-detail, event-detail; vaste regel: geen ads op
+locatie-kiezer/login/account.
+
+**P2-6 · Eigenaar: Onbepaald.** Tekstzoeken op titel op de
+horeca-overzichtspagina (los van de Datatype/Sort-experimenten die Bob
+daar zelf op test — die zijn actief werk-in-uitvoering, geen dode
+code).
+
+**P2-7 · Eigenaar: Bob.** Opschonen:
+- Merge `kaartTabelUitgaanComp` + `kaartTabelUitgaanSComp` — Bob doet
+  dit zelf ("ik kijk er zelf naar").
+- `lib/kanweg` opnieuw leegmaken indien teruggekomen na een latere
+  export-pull, plus eventuele nieuwe losse dode componenten in
+  `lib/evenement/`.
+- Eén browse-by-category-patroon i.p.v. twee: nu Home landelijk is,
+  bepalen hoe Home's categorieën en `PUitgaanPage`'s provincie/
+  gemeente-gescoopte categorieën zich tot elkaar verhouden.
+- Dubbele Provincie/Gemeente-blok in het menu opruimen (12 menu-items
+  waar 6 zouden volstaan).
+- `EventWidget`-route: heraansluiten of definitief schrappen (losse
+  orphan-route).

+ 1 - 1
android/app/build.gradle

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

+ 2 - 2
android/build.gradle

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

+ 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>"; };
 		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>"; };
 		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>"; };
 		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
-		6436409A27A31CD200820AF7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
-		6436409A27A31CDA00820AF7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		6436409B27A31CDB00820AF7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		6436409B27A31CDA00820AF7 /* 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>"; };
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 /* End PBXFileReference section */
 
 
@@ -225,8 +225,8 @@
 		6436409C27A31CD800820AF7 /* InfoPlist.strings */ = {
 		6436409C27A31CD800820AF7 /* InfoPlist.strings */ = {
 			isa = PBXVariantGroup;
 			isa = PBXVariantGroup;
 			children = (
 			children = (
-				6436409A27A31CD200820AF7 /* nl */,
-				6436409A27A31CDA00820AF7 /* en */,
+				6436409B27A31CDB00820AF7 /* nl */,
+				6436409B27A31CDA00820AF7 /* en */,
 			);
 			);
 			name = InfoPlist.strings;
 			name = InfoPlist.strings;
 			sourceTree = "<group>";
 			sourceTree = "<group>";

+ 36 - 12
lib/app_state.dart

@@ -22,8 +22,7 @@ class FFAppState extends ChangeNotifier {
   Future initializePersistedState() async {
   Future initializePersistedState() async {
     secureStorage = FlutterSecureStorage();
     secureStorage = FlutterSecureStorage();
     await _safeInitAsync(() async {
     await _safeInitAsync(() async {
-      _userCsrftoken =
-          await secureStorage.getString('ff_userCsrftoken') ?? _userCsrftoken;
+      _userToken = await secureStorage.getString('ff_userToken') ?? _userToken;
     });
     });
     await _safeInitAsync(() async {
     await _safeInitAsync(() async {
       _userSessionid =
       _userSessionid =
@@ -98,6 +97,9 @@ class FFAppState extends ChangeNotifier {
           await secureStorage.getString('ff_provincieSelectName') ??
           await secureStorage.getString('ff_provincieSelectName') ??
               _provincieSelectName;
               _provincieSelectName;
     });
     });
+    await _safeInitAsync(() async {
+      _userMail = await secureStorage.getString('ff_userMail') ?? _userMail;
+    });
   }
   }
 
 
   void update(VoidCallback callback) {
   void update(VoidCallback callback) {
@@ -108,16 +110,16 @@ class FFAppState extends ChangeNotifier {
   late FlutterSecureStorage secureStorage;
   late FlutterSecureStorage secureStorage;
 
 
   /// csrftoken
   /// csrftoken
-  String _userCsrftoken = '';
-  String get userCsrftoken => _userCsrftoken;
-  set userCsrftoken(String value) {
-    _userCsrftoken = value;
-    secureStorage.setString('ff_userCsrftoken', value);
+  String _userToken = '';
+  String get userToken => _userToken;
+  set userToken(String value) {
+    _userToken = value;
+    secureStorage.setString('ff_userToken', value);
     debugLogAppState(this);
     debugLogAppState(this);
   }
   }
 
 
-  void deleteUserCsrftoken() {
-    secureStorage.delete(key: 'ff_userCsrftoken');
+  void deleteUserToken() {
+    secureStorage.delete(key: 'ff_userToken');
   }
   }
 
 
   String _userSessionid = '';
   String _userSessionid = '';
@@ -458,14 +460,26 @@ class FFAppState extends ChangeNotifier {
     secureStorage.delete(key: 'ff_provincieSelectName');
     secureStorage.delete(key: 'ff_provincieSelectName');
   }
   }
 
 
+  String _userMail = '';
+  String get userMail => _userMail;
+  set userMail(String value) {
+    _userMail = value;
+    secureStorage.setString('ff_userMail', value);
+    debugLogAppState(this);
+  }
+
+  void deleteUserMail() {
+    secureStorage.delete(key: 'ff_userMail');
+  }
+
   Map<String, DebugDataField> toDebugSerializableMap() => {
   Map<String, DebugDataField> toDebugSerializableMap() => {
-        'userCsrftoken': debugSerializeParam(
-          userCsrftoken,
+        'userToken': debugSerializeParam(
+          userToken,
           ParamType.String,
           ParamType.String,
           link:
           link:
               'https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=appValues&appValuesTab=state',
               'https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=appValues&appValuesTab=state',
           searchReference:
           searchReference:
-              'reference=CioKGQoNdXNlckNzcmZ0b2tlbhIIcm1vaGxvcWpyAggDegljc3JmdG9rZW5aDXVzZXJDc3JmdG9rZW4=',
+              'reference=CiYKFQoJdXNlclRva2VuEghybW9obG9xanICCAN6CWNzcmZ0b2tlbloJdXNlclRva2Vu',
           name: 'String',
           name: 'String',
           nullable: false,
           nullable: false,
         ),
         ),
@@ -643,6 +657,16 @@ class FFAppState extends ChangeNotifier {
               'reference=CicKHwoTcHJvdmluY2llU2VsZWN0TmFtZRIIcHMxY2N5Ym9yAggDegBaE3Byb3ZpbmNpZVNlbGVjdE5hbWU=',
               'reference=CicKHwoTcHJvdmluY2llU2VsZWN0TmFtZRIIcHMxY2N5Ym9yAggDegBaE3Byb3ZpbmNpZVNlbGVjdE5hbWU=',
           name: 'String',
           name: 'String',
           nullable: false,
           nullable: false,
+        ),
+        'userMail': debugSerializeParam(
+          userMail,
+          ParamType.String,
+          link:
+              'https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=appValues&appValuesTab=state',
+          searchReference:
+              'reference=ChwKFAoIdXNlck1haWwSCGVhcWNxbTBtcgIIA3oAWgh1c2VyTWFpbA==',
+          name: 'String',
+          nullable: false,
         )
         )
       };
       };
 }
 }

+ 59 - 0
lib/custom_code/actions/drupal_login.dart

@@ -0,0 +1,59 @@
+// Automatic FlutterFlow imports
+import '/backend/schema/structs/index.dart';
+import '/flutter_flow/flutter_flow_theme.dart';
+import '/flutter_flow/flutter_flow_util.dart';
+import 'index.dart'; // Imports other custom actions
+import '/flutter_flow/custom_functions.dart'; // Imports custom functions
+import 'package:flutter/material.dart';
+// Begin custom action code
+// DO NOT REMOVE OR MODIFY THE CODE ABOVE!
+
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+
+Future<dynamic> drupalLogin(
+  String username,
+  String password,
+  String loginUrl,
+) async {
+  try {
+    final response = await http.post(
+      Uri.parse(loginUrl),
+      headers: {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+      },
+      body: jsonEncode({
+        'username': username,
+        'password': password,
+      }),
+    );
+
+    if (response.statusCode == 200) {
+      final data = jsonDecode(response.body);
+
+      return {
+        'success': true,
+        'sessid': data['sessid'],
+        'session_name': data['session_name'],
+        'token': data['token'],
+        'uid': data['user']['uid']?.toString(),
+        'name': data['user']['name'],
+        'mail': data['user']['mail'],
+      };
+    } else {
+      return {
+        'success': false,
+        'error': 'Login failed: ${response.statusCode}',
+        'body': response.body,
+      };
+    }
+  } catch (e) {
+    return {
+      'success': false,
+      'error': e.toString(),
+    };
+  }
+}
+// Set your action name, define your arguments and return parameter,
+// and then add the boilerplate code using the `</>` button on the right!

+ 1 - 0
lib/custom_code/actions/index.dart

@@ -0,0 +1 @@
+export 'drupal_login.dart' show drupalLogin;

+ 1 - 0
lib/custom_code/widgets/from_h_t_m_l.dart

@@ -3,6 +3,7 @@ import '/backend/schema/structs/index.dart';
 import '/flutter_flow/flutter_flow_theme.dart';
 import '/flutter_flow/flutter_flow_theme.dart';
 import '/flutter_flow/flutter_flow_util.dart';
 import '/flutter_flow/flutter_flow_util.dart';
 import 'index.dart'; // Imports other custom widgets
 import 'index.dart'; // Imports other custom widgets
+import '/custom_code/actions/index.dart'; // Imports custom actions
 import '/flutter_flow/custom_functions.dart'; // Imports custom functions
 import '/flutter_flow/custom_functions.dart'; // Imports custom functions
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 // Begin custom widget code
 // Begin custom widget code

+ 22 - 10
lib/evenement/evenement_horecagelegenheid/evenement_horecagelegenheid_widget.dart

@@ -294,16 +294,28 @@ class _EvenementHorecagelegenheidWidgetState
                     itemCount: fotoos.length,
                     itemCount: fotoos.length,
                     itemBuilder: (context, fotoosIndex, _) {
                     itemBuilder: (context, fotoosIndex, _) {
                       final fotoosItem = fotoos[fotoosIndex];
                       final fotoosItem = fotoos[fotoosIndex];
-                      return ClipRRect(
-                        borderRadius: BorderRadius.circular(8.0),
-                        child: CachedNetworkImage(
-                          fadeInDuration: Duration(milliseconds: 500),
-                          fadeOutDuration: Duration(milliseconds: 500),
-                          imageUrl: fotoosItem,
-                          width: 200.0,
-                          height: 200.0,
-                          fit: BoxFit.cover,
-                        ),
+                      return Builder(
+                        builder: (context) {
+                          if (fotoosItem != null && fotoosItem != '') {
+                            return ClipRRect(
+                              borderRadius: BorderRadius.circular(8.0),
+                              child: CachedNetworkImage(
+                                fadeInDuration: Duration(milliseconds: 500),
+                                fadeOutDuration: Duration(milliseconds: 500),
+                                imageUrl: fotoosItem,
+                                width: 200.0,
+                                height: 200.0,
+                                fit: BoxFit.cover,
+                              ),
+                            );
+                          } else {
+                            return Icon(
+                              Icons.image_not_supported,
+                              color: FlutterFlowTheme.of(context).primaryText,
+                              size: 24.0,
+                            );
+                          }
+                        },
                       );
                       );
                     },
                     },
                     carouselController: _model.carouselController ??=
                     carouselController: _model.carouselController ??=

+ 67 - 31
lib/horecagelegenhedenoverzicht/event_current/event_current_widget.dart

@@ -270,16 +270,31 @@ class _EventCurrentWidgetState extends State<EventCurrentWidget>
                           itemCount: fotoos.length,
                           itemCount: fotoos.length,
                           itemBuilder: (context, fotoosIndex, _) {
                           itemBuilder: (context, fotoosIndex, _) {
                             final fotoosItem = fotoos[fotoosIndex];
                             final fotoosItem = fotoos[fotoosIndex];
-                            return ClipRRect(
-                              borderRadius: BorderRadius.circular(8.0),
-                              child: CachedNetworkImage(
-                                fadeInDuration: Duration(milliseconds: 500),
-                                fadeOutDuration: Duration(milliseconds: 500),
-                                imageUrl: fotoosItem,
-                                width: 200.0,
-                                height: 200.0,
-                                fit: BoxFit.cover,
-                              ),
+                            return Builder(
+                              builder: (context) {
+                                if (fotoosItem != null && fotoosItem != '') {
+                                  return ClipRRect(
+                                    borderRadius: BorderRadius.circular(8.0),
+                                    child: CachedNetworkImage(
+                                      fadeInDuration:
+                                          Duration(milliseconds: 500),
+                                      fadeOutDuration:
+                                          Duration(milliseconds: 500),
+                                      imageUrl: fotoosItem,
+                                      width: 200.0,
+                                      height: 200.0,
+                                      fit: BoxFit.cover,
+                                    ),
+                                  );
+                                } else {
+                                  return Icon(
+                                    Icons.image_not_supported,
+                                    color: FlutterFlowTheme.of(context)
+                                        .primaryText,
+                                    size: 24.0,
+                                  );
+                                }
+                              },
                             );
                             );
                           },
                           },
                           carouselController: _model.carouselController ??=
                           carouselController: _model.carouselController ??=
@@ -601,27 +616,48 @@ class _EventCurrentWidgetState extends State<EventCurrentWidget>
                                     },
                                     },
                                   ),
                                   ),
                                 ),
                                 ),
-                                Padding(
-                                  padding: EdgeInsetsDirectional.fromSTEB(
-                                      4.0, 2.0, 4.0, 15.0),
-                                  child: ClipRRect(
-                                    borderRadius: BorderRadius.circular(8.0),
-                                    child: CachedNetworkImage(
-                                      fadeInDuration:
-                                          Duration(milliseconds: 500),
-                                      fadeOutDuration:
-                                          Duration(milliseconds: 500),
-                                      imageUrl: EvenementCall.eventLogog(
-                                        eventCurrentEvenementResponse.jsonBody,
-                                      )!,
-                                      width: MediaQuery.sizeOf(context).width *
-                                          0.15,
-                                      height:
-                                          MediaQuery.sizeOf(context).height *
-                                              0.15,
-                                      fit: BoxFit.cover,
-                                    ),
-                                  ),
+                                Builder(
+                                  builder: (context) {
+                                    if (getJsonField(
+                                          eventCurrentEvenementResponse
+                                              .jsonBody,
+                                          r'''$[0].logo''',
+                                        ) !=
+                                        null) {
+                                      return Padding(
+                                        padding: EdgeInsetsDirectional.fromSTEB(
+                                            4.0, 2.0, 4.0, 15.0),
+                                        child: ClipRRect(
+                                          borderRadius:
+                                              BorderRadius.circular(8.0),
+                                          child: CachedNetworkImage(
+                                            fadeInDuration:
+                                                Duration(milliseconds: 500),
+                                            fadeOutDuration:
+                                                Duration(milliseconds: 500),
+                                            imageUrl: EvenementCall.eventLogog(
+                                              eventCurrentEvenementResponse
+                                                  .jsonBody,
+                                            )!,
+                                            width: MediaQuery.sizeOf(context)
+                                                    .width *
+                                                0.15,
+                                            height: MediaQuery.sizeOf(context)
+                                                    .height *
+                                                0.15,
+                                            fit: BoxFit.cover,
+                                          ),
+                                        ),
+                                      );
+                                    } else {
+                                      return Icon(
+                                        Icons.image_not_supported,
+                                        color: FlutterFlowTheme.of(context)
+                                            .primaryText,
+                                        size: 24.0,
+                                      );
+                                    }
+                                  },
                                 ),
                                 ),
                                 Expanded(
                                 Expanded(
                                   child: wrapWithModel(
                                   child: wrapWithModel(

+ 27 - 12
lib/horecagelegenhedenoverzicht/horecagelegenheid_current/horecagelegenheid_current_widget.dart

@@ -253,18 +253,33 @@ class _HorecagelegenheidCurrentWidgetState
                                 itemCount: fotoos.length,
                                 itemCount: fotoos.length,
                                 itemBuilder: (context, fotoosIndex, _) {
                                 itemBuilder: (context, fotoosIndex, _) {
                                   final fotoosItem = fotoos[fotoosIndex];
                                   final fotoosItem = fotoos[fotoosIndex];
-                                  return ClipRRect(
-                                    borderRadius: BorderRadius.circular(8.0),
-                                    child: CachedNetworkImage(
-                                      fadeInDuration:
-                                          Duration(milliseconds: 500),
-                                      fadeOutDuration:
-                                          Duration(milliseconds: 500),
-                                      imageUrl: fotoosItem,
-                                      width: 200.0,
-                                      height: 200.0,
-                                      fit: BoxFit.cover,
-                                    ),
+                                  return Builder(
+                                    builder: (context) {
+                                      if (fotoosItem != null &&
+                                          fotoosItem != '') {
+                                        return ClipRRect(
+                                          borderRadius:
+                                              BorderRadius.circular(8.0),
+                                          child: CachedNetworkImage(
+                                            fadeInDuration:
+                                                Duration(milliseconds: 500),
+                                            fadeOutDuration:
+                                                Duration(milliseconds: 500),
+                                            imageUrl: fotoosItem,
+                                            width: 200.0,
+                                            height: 200.0,
+                                            fit: BoxFit.cover,
+                                          ),
+                                        );
+                                      } else {
+                                        return Icon(
+                                          Icons.image_not_supported,
+                                          color: FlutterFlowTheme.of(context)
+                                              .primaryText,
+                                          size: 24.0,
+                                        );
+                                      }
+                                    },
                                   );
                                   );
                                 },
                                 },
                                 carouselController:
                                 carouselController:

+ 10 - 12
lib/login/login/login_model.dart

@@ -1,12 +1,10 @@
-import '/backend/api_requests/api_calls.dart';
-import '/backend/api_requests/api_streaming.dart';
 import '/components/header_buttons_component_widget.dart';
 import '/components/header_buttons_component_widget.dart';
 import '/flutter_flow/flutter_flow_theme.dart';
 import '/flutter_flow/flutter_flow_theme.dart';
 import '/flutter_flow/flutter_flow_util.dart';
 import '/flutter_flow/flutter_flow_util.dart';
 import '/flutter_flow/flutter_flow_widgets.dart';
 import '/flutter_flow/flutter_flow_widgets.dart';
 import '/shared/drawer_component/drawer_component_widget.dart';
 import '/shared/drawer_component/drawer_component_widget.dart';
-import 'dart:convert';
 import 'dart:ui';
 import 'dart:ui';
+import '/custom_code/actions/index.dart' as actions;
 import '/index.dart';
 import '/index.dart';
 import 'login_widget.dart' show LoginWidget;
 import 'login_widget.dart' show LoginWidget;
 import 'package:easy_debounce/easy_debounce.dart';
 import 'package:easy_debounce/easy_debounce.dart';
@@ -28,14 +26,14 @@ class LoginModel extends FlutterFlowModel<LoginWidget> {
   TextEditingController? passwordFieldTextController;
   TextEditingController? passwordFieldTextController;
   late bool passwordFieldVisibility;
   late bool passwordFieldVisibility;
   String? Function(BuildContext, String?)? passwordFieldTextControllerValidator;
   String? Function(BuildContext, String?)? passwordFieldTextControllerValidator;
-  // Stores action output result for [Backend Call - API (login)] action in Button widget.
-  ApiCallResponse? _apiResultlco;
-  set apiResultlco(ApiCallResponse? value) {
-    _apiResultlco = value;
+  // Stores action output result for [Custom Action - drupalLogin] action in Button widget.
+  dynamic? _resultDrupalLogin;
+  set resultDrupalLogin(dynamic? value) {
+    _resultDrupalLogin = value;
     debugLogWidgetClass(this);
     debugLogWidgetClass(this);
   }
   }
 
 
-  ApiCallResponse? get apiResultlco => _apiResultlco;
+  dynamic? get resultDrupalLogin => _resultDrupalLogin;
 
 
   // Model for drawerComponent component.
   // Model for drawerComponent component.
   late DrawerComponentModel drawerComponentModel;
   late DrawerComponentModel drawerComponentModel;
@@ -88,12 +86,12 @@ class LoginModel extends FlutterFlowModel<LoginWidget> {
           )
           )
         }.entries,
         }.entries,
         actionOutputs: {
         actionOutputs: {
-          'apiResultlco': debugSerializeParam(
-            apiResultlco,
-            ParamType.ApiResponse,
+          'resultDrupalLogin': debugSerializeParam(
+            resultDrupalLogin,
+            ParamType.JSON,
             link:
             link:
                 'https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=uiBuilder&page=login',
                 'https://app.flutterflow.io/project/uitgaanskrant-1qhvtd?tab=uiBuilder&page=login',
-            name: 'ApiCallResponse',
+            name: 'dynamic',
             nullable: true,
             nullable: true,
           )
           )
         }.entries,
         }.entries,

+ 17 - 16
lib/login/login/login_widget.dart

@@ -1,12 +1,10 @@
-import '/backend/api_requests/api_calls.dart';
-import '/backend/api_requests/api_streaming.dart';
 import '/components/header_buttons_component_widget.dart';
 import '/components/header_buttons_component_widget.dart';
 import '/flutter_flow/flutter_flow_theme.dart';
 import '/flutter_flow/flutter_flow_theme.dart';
 import '/flutter_flow/flutter_flow_util.dart';
 import '/flutter_flow/flutter_flow_util.dart';
 import '/flutter_flow/flutter_flow_widgets.dart';
 import '/flutter_flow/flutter_flow_widgets.dart';
 import '/shared/drawer_component/drawer_component_widget.dart';
 import '/shared/drawer_component/drawer_component_widget.dart';
-import 'dart:convert';
 import 'dart:ui';
 import 'dart:ui';
+import '/custom_code/actions/index.dart' as actions;
 import '/index.dart';
 import '/index.dart';
 import 'package:easy_debounce/easy_debounce.dart';
 import 'package:easy_debounce/easy_debounce.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -436,34 +434,37 @@ class _LoginWidgetState extends State<LoginWidget> with RouteAware {
                           ),
                           ),
                           FFButtonWidget(
                           FFButtonWidget(
                             onPressed: () async {
                             onPressed: () async {
-                              _model.apiResultlco = await LoginCall.call(
-                                username:
-                                    _model.usernameFieldTextController.text,
-                                password:
-                                    _model.passwordFieldTextController.text,
+                              _model.resultDrupalLogin =
+                                  await actions.drupalLogin(
+                                _model.usernameFieldTextController.text,
+                                _model.passwordFieldTextController.text,
+                                'https://uitgaanskrant.com/en/flutterdrup/user/login.json',
                               );
                               );
-
-                              if ((_model.apiResultlco?.succeeded ?? true)) {
+                              if (_model.resultDrupalLogin!) {
                                 FFAppState().userSessionid = getJsonField(
                                 FFAppState().userSessionid = getJsonField(
-                                  (_model.apiResultlco?.jsonBody ?? ''),
+                                  _model.resultDrupalLogin,
                                   r'''$.sessid''',
                                   r'''$.sessid''',
                                 ).toString();
                                 ).toString();
                                 FFAppState().userSessionname = getJsonField(
                                 FFAppState().userSessionname = getJsonField(
-                                  (_model.apiResultlco?.jsonBody ?? ''),
+                                  _model.resultDrupalLogin,
                                   r'''$.session_name''',
                                   r'''$.session_name''',
                                 ).toString();
                                 ).toString();
-                                FFAppState().userCsrftoken = getJsonField(
-                                  (_model.apiResultlco?.jsonBody ?? ''),
+                                FFAppState().userToken = getJsonField(
+                                  _model.resultDrupalLogin,
                                   r'''$.token''',
                                   r'''$.token''',
                                 ).toString();
                                 ).toString();
                                 FFAppState().userName = getJsonField(
                                 FFAppState().userName = getJsonField(
-                                  (_model.apiResultlco?.jsonBody ?? ''),
+                                  _model.resultDrupalLogin,
                                   r'''$.user.name''',
                                   r'''$.user.name''',
                                 ).toString();
                                 ).toString();
                                 FFAppState().userUid = getJsonField(
                                 FFAppState().userUid = getJsonField(
-                                  (_model.apiResultlco?.jsonBody ?? ''),
+                                  _model.resultDrupalLogin,
                                   r'''$.user.uid''',
                                   r'''$.user.uid''',
                                 );
                                 );
+                                FFAppState().userMail = getJsonField(
+                                  _model.resultDrupalLogin,
+                                  r'''$.user.mail''',
+                                ).toString();
                                 safeSetState(() {});
                                 safeSetState(() {});
                               } else {
                               } else {
                                 ScaffoldMessenger.of(context).showSnackBar(
                                 ScaffoldMessenger.of(context).showSnackBar(