Skip to content

Napraw crash strony wypadu: hook useMemo wołany warunkowo po wczesnym return - #19

Open
uncleeai wants to merge 292 commits into
claude/claude-md-docs-66ys0zfrom
claude/event-page-redesign
Open

Napraw crash strony wypadu: hook useMemo wołany warunkowo po wczesnym return#19
uncleeai wants to merge 292 commits into
claude/claude-md-docs-66ys0zfrom
claude/event-page-redesign

Conversation

@uncleeai

Copy link
Copy Markdown
Owner

nag był obliczany useMemo po 'if (loading) return', więc przy przejściu
loading true->false rosła liczba hooków i React się wywalał. Przeniesiono
useMemo nad wczesne returny, do reszty hooków.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2

claude and others added 30 commits June 24, 2026 18:56
- Włącz experimental.viewTransition w next.config
- Komponent PageTransition owijający treść strony w React <ViewTransition>
  z mapą enter/exit dla typów nav-forward / nav-back
- transitionTypes na linkach: wejście w wypad = nav-forward (slide z prawej),
  powrót do listy = nav-back (slide z lewej); router.push też taguje forward
- CSS kierunkowych slajdów + poszanowanie prefers-reduced-motion
- Typy canary Reacta (ViewTransition) włączone przez referencję w d.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
…integracji React

- Provider w layoucie (nie odmontowuje się przy nawigacji) owija router.push w
  document.startViewTransition i kończy animację po zmianie pathname
- Kierunek przez data-vt na <html>; CSS animuje ::view-transition-old/new(root)
  (forward: nowa z prawej na wierzchu; back: bieżąca zjeżdża w prawo, odsłania poprzednią)
- Linki/karty wołają useTransitionNavigate (z obsługą Cmd/Ctrl-klik i reduced-motion)
- Usunięto experimental.viewTransition, komponent PageTransition i typy canary

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
- GlassBackground: natywna pętla zamiast per-klatkowego seeku currentTime
  (najcięższy koszt na iOS); pauza wideo gdy karta w tle
- Hoist tła do layoutu — jedna persystentna instancja zamiast remountu i reloadu
  wideo przy każdej nawigacji (płynniejsze też przejścia stron)
- Wideo na własnej warstwie GPU (translateZ/will-change), blur 60px -> 44px
  (wygląd ~ten sam, taniej)
- contain: layout paint na .form-collapse — animacja rozwijania nie reflowuje
  całej listy wypadów

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
…nelem

Po przeniesieniu tła do body z-index:-1 chował je za nieprzezroczystym tłem body
(aurora widoczna tylko poniżej treści). Tło na z-index:0, .container z treścią na
z-index:1 — aurora wypełnia ekran za zawartością jak wcześniej.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
Rozmycie szkła nad odtwarzanym wideo przelicza się co klatkę dla każdej karty.
Karty listy (.event-rich) powtarzają się N razy — zamiast frostowania dają teraz
ciemne półprzezroczyste tło (panel). Duże karty i CTA zostają szklane. Duży zysk
na głównym ekranie przy minimalnej zmianie wyglądu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
- BackgroundProvider (kontekst + zapamiętywanie w localStorage)
- GlassBackground respektuje preferencję (wyłączone = brak wideo, czarne tło)
- Przełącznik (iOS-owy switch) w modalu profilu, pod wyborem awatara
- Wyłączenie tła wyraźnie odciąża słabsze urządzenia (brak dekodu/blura wideo,
  szkło nad statycznym tłem liczone raz zamiast co klatkę)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
Duża karta formularza nowego wypadu rozmywała tło co klatkę w trakcie animacji
wysokości — zamiana na ciemny kryjący panel (jak karty listy) usuwa ten koszt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
To była percepcja, nie realny lag — wracamy do backdrop-filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
- Lekki singleton w pamięci (dataCache): dashboard zapisuje komplet danych,
  strona wypadu seeduje z niego stan na start -> brak 'Wczytuję...' przy wejściu z listy
- Strona wypadu po świeżym load() odświeża cache (mergeEventData), więc powrót na
  listę pokazuje aktualne dane
- Dashboard też seeduje z cache (instant powrót)
- Realtime i load() bez zmian — głosy/zmiany dalej aktualizują się na żywo;
  cache to tylko zaliczka na render, nie zamiennik pobierania

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
Rozwiązuje przeskok na pętli i okazjonalny freeze wideo. Dwie warstwy radialnych
gradientów animowane tylko transformem (GPU), pętla alternate = bezszwowa.
Koszt znikomy, brak dekodowania wideo. Przełącznik tła w profilu działa jak dotąd.
Plik wideo zostaje w repo na wypadek powrotu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
Zamiast reagować na mysz (--posX/--posY z pointermove), zmienne są rejestrowane
przez @Property i animowane dwiema pętlami alternate o różnych czasach — organiczny,
bezszwowy ruch tła. Nadal tani (brak wideo), przełącznik tła działa.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
- filter: blur(80px) + opacity 0.45 (więcej rozmycia, ciemniej)
- gradient statyczny, ruch przez translate/scale (GPU) — taniej niż animowanie
  pozycji gradientu; blur liczony raz
- mocny blur maskuje 'ściśnięcie' radialnych gradientów na wąskim ekranie telefonu

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF3pEjMiVuE3XnzgNeXfR2
- Usuń nagłówek „Planner" i tekst pod nim z ekranu logowania.
- Wyśrodkuj box logowania w pionie (.auth-screen), karta w szklanym stylu
  (klasa glass-page) — spójnie z resztą apki. Przycisk gościa zostaje.
- Przywróć tło wideo, ale wyłącznie na ekranie logowania (LoginBackground):
  jedna karta, krótka chwila. Globalna aurora chowana klasą body.login-active,
  żeby nie liczyć dwóch teł naraz; reszta apki bez zmian. Szanuje przełącznik
  tła i pauzuje wideo, gdy karta jest w tle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Slide na ciężkich szklanych ekranach zacinał pierwszą klatkę, a próby wyłączenia
tła ze snapshotu odbijały się czernią/przeskokiem po zescrollowaniu (ograniczenie
WebKita dla nazwanych elementów fixed). Crossfade nie przesuwa nic, więc jest
odporny na scroll i bez artefaktów pozycyjnych, a ewentualny koszt snapshotu jest
niewidoczny. Nowa strona pojawia się nad starą (opacity), strony są nieprzezroczyste
więc przenikanie jest czyste. Slide'owe keyframes i zależność od kierunku usunięte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Crossfade/slide oparte na View Transitions snapshotowały całą stronę, a snapshot
przewiniętej strony kotwiczy się do góry dokumentu → na starcie przejścia treść
skakała (stutter widoczny po zescrollowaniu). Problem jest wpisany w snapshoty,
niezależnie od typu animacji.

Rezygnujemy ze snapshotów: nawigacja to zwykły router.push, a nowa strona animuje
się przy zamontowaniu (keyframes page-in: lekki unos + pojawienie) czystym
transform/opacity na <main>. Brak kosztu rasteryzacji, odporne na scroll, bez
przeskoku. Tracimy zanikanie starej strony, ale to stabilny, standardowy wzorzec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Przy otwieraniu wypadu z przewiniętej listy strona wypadu montowała się, gdy okno
było jeszcze przewinięte w dół: przez klatkę widać było pustkę poniżej treści, a na
iOS fixed tło potrafiło mignąć na czarno przy reflowie, po czym scroll przeskakiwał
na górę. Otwierane z góry (scroll 0) — bez problemu.

navigate() resetuje teraz scroll do zera synchronicznie tuż przed router.push, więc
strona wypadu montuje się już przy scroll 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Diagnoza: kod animacji nie zmienił się od 9ce501e — slide stutter/mignięcie czerni
brały się z TŁA. Po 9ce501e wideo zamieniono na ciężką aurorę CSS (blur 80px +
background-blend-mode), która jest droga w przemalowaniu: każda nawigacja/reset
scrolla wymuszały reflow → zacięcie, a fixed element na klatkę nie zdążył się
namalować → czerń. Wideo jako warstwa GPU przemalowuje się tanio.

- transition.tsx + GlassBackground.tsx przywrócone 1:1 z 9ce501e (slide VT + wideo).
- globals.css: .glass-aurora (+ keyframes aurora-*) → .glass-video; animacja wejścia
  (page-in) → slide ::view-transition (root) z data-vt.
- Reset scrolla przed nawigacją zniknął wraz z powrotem do oryginalnego transition.tsx.
- Przeprojektowany ekran logowania zostaje bez zmian.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Push (działa też przy zamkniętej apce; iOS tylko w PWA na ekranie głównym, 16.4+):
- public/sw.js — service worker (push + notificationclick → otwiera wypad).
- src/lib/push.ts — subskrypcja: rejestracja SW, zgoda, zapis do bazy; wsparcie
  i wykrycie standalone.
- ProfileMenu — przełącznik „Powiadomienia" (gdy wspierane); poza standalone na iOS
  podpowiedź „dodaj do ekranu głównego".
- supabase/schema.sql — tabela push_subscriptions + RLS (każdy zarządza swoimi).
- supabase/functions/notify-new-event — Edge Function (Deno/web-push) wyzwalana
  Database Webhookiem na INSERT events; rozsyła push do wszystkich poza twórcą,
  sprząta martwe subskrypcje (404/410); chroniona nagłówkiem x-webhook-secret.

Toast na żywo (gdy apka otwarta), bez żadnej konfiguracji:
- NewEventToast w auth.tsx — Realtime na INSERT events, pływający baner + link.

Dodatkowo: NEXT_PUBLIC_VAPID_PUBLIC_KEY w .env.example, instrukcja konfiguracji
w README, tsconfig wyklucza supabase/functions z type-checku Next, aktualizacja CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Toast łapał tylko pierwszy wypad po otwarciu apki: dashboard i toast miały DWA
osobne kanały Realtime na tej samej tabeli `events`, a Supabase dostarczał zmiany
tylko jednemu. Teraz jest jedna globalna subskrypcja `events` (w NewEventToast),
która rozgłasza zmiany zdarzeniem `planner:events-changed`; dashboard słucha go
zamiast subskrybować `events` osobno.

Dodatkowo:
- Edge Function: czysta wersja bez logów debug; sekret webhooka opcjonalny i
  sprawdzany z query param `?key=` lub nagłówka (query param jest niezawodny).
- README: pomysły na dalej — opis wyjazdu (kolumna description już jest),
  przypomnienia (pg_cron + push) i ping od organizatora do niegłosujących.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Opis wypadu:
- pole „Opis (opcjonalnie)" (textarea) w obu wariantach formularza „Nowy wypad",
  zapisywane do istniejącej kolumny events.description,
- wyświetlanie opisu na stronie wypadu (.event-description, zachowuje przełamania).

Ustawienia:
- nowy komponent SettingsMenu (ikona koła zębatego, IconGear) renderowany obok
  avatara; przeniesione tam przełączniki „Animowane tło" i „Powiadomienia",
- ProfileMenu odchudzone — zostaje awatar/zdjęcie, nick i wylogowanie.

Style: textarea (spójny z inputami) i .settings-trigger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Edge Function notify-reminders odpalana przez pg_cron: dla wypadów starszych niż
24h, z terminem w przyszłości i bez znacznika reminded_at, wysyła push do osób
z paczki, które nie oddały głosu (poza twórcą), po czym ustawia events.reminded_at
— jedno przypomnienie na wypad. Filtr 24h–14 dni, żeby nie zalewać starymi.

- schema.sql: kolumna events.reminded_at.
- supabase/functions/notify-reminders: logika + opcjonalny sekret przez ?key=.
- README: instrukcja (deploy, pg_cron + pg_net, harmonogram cron.schedule).
- CLAUDE.md: struktura + model danych.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Po wyczyszczeniu bazy lub zalogowaniu na inne konto przeglądarka miała subskrypcję,
ale w `push_subscriptions` brakowało wiersza (albo był na starym koncie) — push
przestawał przychodzić do czasu ręcznego przeklikania OFF/ON.

resyncPushSubscription(userId) po cichu upsertuje istniejącą subskrypcję przeglądarki
pod aktualne konto; wołane w AuthProvider przy każdym zalogowaniu. Nie prosi o zgodę
ani nie rejestruje SW — działa tylko gdy subskrypcja już istnieje.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
Bug: na części iOS (też topowych) tap w event czasem nie przenosił na stronę wypadu
— zostawał na home, a slajd leciał „home → home". Przyczyna: router.push odpalany
WEWNĄTRZ startViewTransition bywał porzucany przez Safari (callback przejścia jest
traktowany specjalnie), więc nawigacja nie dochodziła.

Rezygnujemy ze snapshotów: navigate() to zwykły router.push (nigdy nie zawiedzie),
a nowa strona animuje się przy zamontowaniu (keyframes page-in: lekki unos + fade)
na <main>. Bulletproof na każdym urządzeniu, brak wyścigów. Tracimy wyjazd starej
strony — świadomy kompromis na rzecz niezawodności.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
page-in: translateX(48px)→0 + fade, 0.32s (zamiast 12px w górę) — bliżej dawnego
iOS-owego slajdu, ale nadal czysty CSS bez snapshotów. .container przycina poziom,
żeby wjazd nie dawał bocznego scrolla (brak position: sticky, więc bezpieczne).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
hidden + overflow-y: visible wymusza wg spec CSS overflow-y: auto → .container
stawał się pionowym kontenerem scrolla (footgun przy przewijaniu/scroll-into-view).
clip przycina poziom przy wjeździe strony, nie tworząc scrollera.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
…(A+B)

Rozdzielenie „prowadzącego terminu" (na żywo, informacyjnie) od „ustalonego"
(wypad zdecydowany → trafia do Nadchodzące). Jeden głos „Wchodzę" już NIE przenosi
wypadu z „W trakcie" ani nie chowa paska frekwencji.

getEventStatus (types.ts): ustalone gdy
  A) organizator zapisał confirmed_slot_id (pierwszeństwo, sticky), albo
  B) WSZYSCY z paczki (łącznie z organizatorem) dali znać i jest prowadzący termin.
Liczone na żywo → zmiana głosu przestawia/cofa automat; ręczne trzyma się do odznaczenia.

Dashboard: sekcje open/upcoming/past wg getEventStatus; pasek „% dało znać" zostaje
w „W trakcie" + podpowiedź prowadzącego terminu („… prowadzi · N wchodzi").
Strona wypadu: przyciski organizatora „Ustal ten termin"/„Odznacz", odznaka „✓ Ustalony",
nagłówek rozróżnia „Ustalone" (auto/ręczne) od „Na czele".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
- „Minione" → „Odbyte" (ustalone, po dacie; plakietka „✓ Odbyte").
- Nowa sekcja „Nie ustalono": wypady nieustalone, których WSZYSTKIE terminy minęły.
  Pokazywane do 24h po ostatnim terminie (plakietka „Nie ustalono", „Termin minął"),
  potem znikają z listy (archiwizacja klient-side — rekord zostaje w bazie, nic nie
  kasujemy; z bezpośredniego linku dalej się otwiera).
- „W trakcie" trzyma tylko wypady z przyszłym terminem lub bez terminów.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qgyoEJ3gYpX2gADzgM7JZ
claude and others added 24 commits July 19, 2026 11:22
Poprzednia wersja zwijała godzinę, a data WSKAKIWAŁA w jej miejsce (pola
dosunięte do prawej). Teraz blok pól Start ma stałą szerokość (216px),
a animują się proporcje kolumn grida (1fr:0.58fr ↔ 1fr:0fr) + gap —
pill daty rośnie i pochłania miejsce po godzinie jak jeden przelewający
się kształt. Wiersz Koniec zostaje na flexie (jeden pill).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
Bazowe .slot-range .dt-field { width: auto } + input { width: 100% } robiło
błędne koło (wrapper od contentu, input od wrappera → pill zapadał się do
zera, „Godz." wystawało poza tło). W siatce dual wrapper wypełnia komórkę.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
…wkt1r

Własne tło wypadu: upload, pinch-to-crop i edycja przez kreator
Fundament galerii zdjęć wypadu (pliki w Cloudflare R2, metadane w Supabase):
- tabela event_photos (wypad, autor, ścieżki podgląd/oryginał, taken_at)
  z RLS jak komentarze (czyta paczka, wstawia autor, kasuje autor/organizator/
  admin) + realtime; zaaplikowana na produkcję,
- Edge Function sign-photo-upload (verify JWT): presigned PUT do R2 przez
  aws4fetch, ścieżki budowane server-side, URL-e ważne 10 min; wdrożona,
- lib/gallery.ts: podgląd JPEG 2048px na telefonie, upload par
  (oryginał bajt-w-bajt + podgląd), wpisy metadanych, flaga
  isGalleryConfigured (czeka na publiczny URL bucketa).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
- EventGallery na stronie wypadu: sekcja „Zdjęcia" z siatką 3×N podglądów
  (lazy), kafelek „+" z multi-selectem i postępem x/y, realtime (zdjęcia
  wskakują u wszystkich na żywo).
- Pełnoekranowy viewer: swipe między zdjęciami, licznik, podpis (autor +
  data zrobienia), „Oryginał" (pobranie bajt-w-bajt z R2) i „Usuń"
  (autor/organizator/admin, RLS pilnuje po stronie bazy).
- Dashboard: odbyte wypady z fotkami dostają znaczek „📸 N" obok GG.
- R2_PUBLIC_BASE ustawione na publiczny URL bucketa (r2.dev — dla skali
  paczki w zupełności; upgrade = własna domena i jedna stała).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
Debug „nie udało się wysłać przesyłki": w logach Supabase ZERO requestów do
sign-photo-upload (bezpośredni test funkcji: OPTIONS 200 + 401 bez tokena,
oba logowane) — błąd działał się w przeglądarce przed siecią, najpewniej przy
generowaniu podglądu. Rura przezbrojona per plik: podgląd z fallbackiem
(padnie → oryginał robi za preview), podpis + PUT-y + wiersz metadanych
osobno dla każdego zdjęcia (częściowy sukces zostaje), komunikaty błędów
wskazują etap (podpis / R2 / metadane) i lecą też do console.error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
functions.invoke zawijał prawdziwą przyczynę w „Failed to send a request to
the Edge Function". Goły fetch na /functions/v1/sign-photo-upload z jawnym
tokenem sesji i anon key: odrzucenie sieci, status HTTP i treść błędu lecą
wprost do alertu (i konsoli) — wiadomo, gdzie boli.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
„Load failed" na iPhonie przy działającym REST na tej samej domenie =
podpis content blockera Safari; ścieżka zawierała „photo-upload" — wzorzec
z list filtrów. Ta sama funkcja pod neutralnym slugiem (wdrożona), klient
przepięty, stara nazwa usunięta z repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
Backend zweryfikowany end-to-end prawdziwym tokenem użytkownika (sign →
presigned PUT do R2 → odczyt z pub URL, wszystko 200) — klucze R2 i funkcja
są dobre; pada wyłącznie fetch ze strony na preview. Klient próbuje teraz
dwóch adresów tej samej funkcji (główny + <ref>.functions.supabase.co) —
inna nazwa hosta omija filtry URL; błąd raportuje oba.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQ2tgPgR9wwC3eT5DbjfC7
Photo upload failed on iOS Safari with "Podpis wysyłki: sieć odrzuciła
żądanie (Load failed)" — a network-level fetch TypeError. Verified the
gallery-sign Edge Function is healthy (curl: OPTIONS 200 + CORS, invalid
JWT → readable 401 + CORS; a valid signed upload appears in the function
logs), yet real user attempts never reach Supabase at all. The browser's
cross-origin call straight to *.supabase.co dies before leaving the device
— the class of failure content blockers, iCloud Private Relay and iOS
preflight quirks cause for third-party requests. Earlier client-side
workarounds (function rename, legacy-domain fallback) couldn't fix that.

Add a Next.js API route that the browser calls same-origin
(/api/gallery-sign) and which forwards server-side to the Edge Function.
Same-origin means no CORS/preflight and no third-party request, so blockers
have nothing to match; the server→Supabase hop is as reliable as curl. The
user's JWT is forwarded, so the function still verifies identity and R2
keys stay in Supabase. Client drops the fragile dual-domain fetch for a
single call to the proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
When the Edge Function gateway returns 404 ("Requested function was not
found"), the real cause is almost always that NEXT_PUBLIC_SUPABASE_URL
points at a project where gallery-sign isn't deployed. Replace the raw
gateway JSON with a clear message naming the project ref we forwarded to,
so the mismatch is obvious from the app itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Uploads run sequentially, so picking too many photos meant waiting them all
out. Thread an AbortSignal through uploadEventPhotos: it's checked before
each file and passed to the sign/R2 fetches. While uploading, the "+" tile
becomes a stop button (accent square + done/total) that aborts on tap —
already-uploaded photos stay, the rest never start, and a user-initiated
stop is not reported as an error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
…le style

Viewer now pages between photos with a finger-tracking swipe: a 3-slide
track (prev/current/next) follows the drag and, on release, eases to the
neighbor past a threshold (or springs back) with a cubic-bezier transition,
then commits the index seamlessly. Edge resistance when there's no neighbor.

Also revert the upload stop-tile to its previous look — plain mono
"done/total…" text instead of the accent square; it stays tappable to abort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Deleting a photo now soft-deletes (sets event_photos.deleted_at): it
vanishes from the gallery immediately but the R2 files stay as a buffer.
A new pg_cron Edge Function gallery-gc runs daily and, past a 30-day TTL,
deletes both R2 objects (idempotent S3 DELETE via aws4fetch) and then the
row — a failed R2 delete keeps the row for the next run, so no orphans.

- schema: event_photos.deleted_at column, partial trash index, UPDATE
  policy (same actors as delete) for the soft delete.
- client: deleteEventPhoto UPDATEs deleted_at; gallery load and dashboard
  photo counts filter deleted_at is null.
- function: gallery-gc (no-verify-jwt, optional WEBHOOK_SECRET), service
  role, batch of 200, only purges rows whose R2 objects are confirmed gone.

Applied to the live project: migration, function deploy, daily cron, and an
end-to-end purge test (R2 token confirmed to allow DELETE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
The fixed-duration paging felt like a hard jump on release. Two fixes:
- Keep the transform transition always defined and drive it via
  transitionDuration (0ms while tracking the finger, >0 on release).
  Toggling the transition on in the same frame as the value change made
  Safari skip the animation, snapping instead of easing.
- Compute the settle duration from the gesture's release velocity so a
  flick continues fast and a slow drag eases gently (iOS-like momentum),
  with a soft deceleration curve. Fast flicks also commit below the
  distance threshold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Drop the 6px slide padding that showed a black seam between photos while
swiping. Settle animation reworked for smoothness: ease-out curve with a
long deceleration tail (0.16, 1, 0.3, 1) and longer velocity-scaled
durations (300-560ms), so release starts at finger speed and glides to a
stop instead of snapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
CSS-transition paging can't be grabbed mid-flight, which forced blocking
new swipes until the animation ended and never felt native. Replace it with
a rAF-driven critically damped spring: position/velocity live in refs, the
transform is written directly to the element (no per-frame setState), and
the settle starts from the smoothed finger velocity, so release glides in
the gesture's own tempo. A new touch cancels the spring and picks the photo
up exactly where it is. Index commits immediately on release with a
width-compensated position (same visual frame), so there is no pending
state and rapid successive flicks chain naturally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Slides were keyed by slot (0/1/2), so the immediate index commit on
release mutated src on both visible <img> elements mid-animation, forcing
re-decode right when the spring runs — dropped frames. Key slides by photo
index instead: React moves the already-decoded elements and only mounts
the one new offscreen neighbor, decoded off the main thread
(decoding="async").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Restore the seam between photos, but as a flex gap on the track instead of
slide padding — the current photo still reaches the screen edges and the
16px gap is only visible during the transition, like iOS Photos. The page
stride and centering account for the gap (width + gap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Swipe a comment row left to reveal a red "Usuń" button (76px, with
rubber-band resistance past it and a haptic tick on open); tap it to run
the existing confirm+delete flow. Content tracks the finger via direct
style writes, release settles through the CSS transition, and React state
pins the open/closed result. Horizontal-vs-vertical intent detection keeps
page scroll working (touch-action: pan-y), and a >12px horizontal move
already cancels the reaction long-press, so gestures don't collide.
Available to the same people as delete (author/organizer/admin); opening
another row or tapping the content closes the revealed button. On coarse
pointers the header ✕ is hidden — swipe replaces it; desktop keeps ✕.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Swiping a just-posted comment during the optimistic window grabbed a row
that was about to be replaced by its saved version (new id, new DOM node),
causing a visual jump. Gate the swipe handlers on `saved`, mirroring the
reaction long-press, so the gesture only binds to a stable element.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
The event page showed no photo at all: the hero image logic (custom
image_url with focus, or the curated category photo from the emoji plus
its hero_crops framing) only existed in the dashboard cards, so opening an
event dropped all of its visual identity. Past events were worse: a dashed
"already happened" banner dominated, the full ready check listed stale
proposals, and the photos - the reason you revisit - sat at the bottom.

- New EventHero component renders the photo full-bleed at the top (bleeding
  past the container padding and safe areas), with floating back/edit
  buttons over it and the title plus meta below, left-aligned. Reuses the
  dashboard's photo layers and framing. Events without a photo or a mapped
  emoji fall back to the previous text header rather than an empty frame.
- Past events: gallery moves directly under the hero (unlabelled, the grid
  speaks for itself), the ready check collapses to the settled slot alone
  under a "Kto był" label, and the banner becomes a quiet "Odbyło się"
  chip next to the meta. The calendar chip hides once the event is over.
- Gallery gained an empty state, which it never had; it would have read as
  broken now that it leads the past-event screen.

Two fixes found while verifying in a browser: .back-btn-round kept the
global button padding (13px 18px) against its fixed 38px box, which
squeezed the icon to zero width as soon as it was used on a <button>; and
the hero's grain and blur, tuned for a small card, looked dirty at this
scale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
The fade hit rgba(0,0,0,0.55) at 74% of the frame, so the bottom quarter
of the photo died into flat black and the transition read as a wall rather
than a gradient; the remaining 18px margin then left a band of empty black
above the title.

Hold the frame clean to ~62% and move the whole falloff to the last fifth,
with the closing stops mixed on the page background (--bg) instead of pure
black so it doesn't haze over the dark page. Pull the title block up 36px
so it sits inside that falloff: the photo now runs into the title instead
of ending above it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
planner Ready Ready Preview, Comment Jul 27, 2026 7:52pm

A darkening overlay can never fully hide where the photo stops: at the
bottom edge the image is cut at full opacity underneath, and on bright
parts of the frame that seam stays visible however gentle the gradient is.

Fade the photo itself with a mask-image down to fully transparent, so the
alpha reaches zero exactly at the edge and there is nothing left to cut.
The falloff starts around the middle of the frame and uses closely spaced
stops approximating an ease curve, which keeps it from banding. The old
overlay stays only as a small top scrim so the floating buttons keep their
contrast on bright photos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Hold full opacity only through the top ~18% and spread the falloff across
the rest, so the photo dissolves into the page over most of its height
instead of over the bottom third.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5vnmcgfqY6rNjwwjiE3fB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants