feat(wallet): enhance wallet UI and transaction item display
fix(profile): update profile detail card to handle loading and error states
style: improve UI consistency across profile and wallet components
Dockerfile: deps -> builder -> runner. NEXT_PUBLIC_* vars are passed as
build args (inlined into the client bundle at build time, per Next.js);
everything else is read at container runtime instead (docker-compose.yml's
env_file / docker run --env-file) and never baked into an image layer.
Runner stage is non-root, ships only server.js + .next/static + public/
via output: "standalone".
Verified by actually running the build twice locally (Docker itself isn't
available in this environment) -- once with the real .env.local, once with
only placeholder values and no .env.local at all, matching the real Docker
build condition. The second run caught a real, pre-existing bug that had
nothing to do with Docker specifically: /login and three other
client-component trees (location/error, location/permission,
CategoryChips and NeighborhoodSearch nested in Server Component pages) all
call useSearchParams() without a Suspense boundary. next dev tolerates
this; next build hard-fails on it ("missing-suspense-with-csr-bailout").
This would have broken any production build -- Vercel, bare metal,
whatever -- not just Docker; fixed all four by wrapping in <Suspense>.
Also added .env.example (committed, no real values -- .env.local itself
stays gitignored) and a docker-compose.yml, with the --env-file .env.local
requirement called out explicitly in README.md since Compose only
auto-reads a file literally named .env, not .env.local.
Root cause of the home-page 500 ("اطلاعات برای اعتبارسنجی ارسال نشده است",
401 not_authenticated): getSession() only decrypted the session cookie, it
never checked whether the access token inside it had actually expired. The
JWE wrapper lives 30 days; the real Gooyal access token lives ~10 hours
(expires_in: 36000). So isAuthenticated stayed true long after the token
died, the home page called getCart() anyway, winofyFetch's refresh attempt
failed silently (dead refresh token) and returned no Authorization header
at all, and the resulting 401 was never caught -- crashing the whole page.
Fix: getValidSession() (session.ts) is now the single source of truth --
refreshes when possible, self-heals by clearing the cookie when refresh
fails, deduped per-request via React's cache(). winofyFetch and every
isAuthenticated check (home page, shop layout) now use it instead of the
raw cookie read.
That surfaced a second bug: Next.js forbids writing cookies during a plain
Server Component render (Server Actions/Route Handlers only), so
getValidSession()'s self-heal itself crashed when called from a page like
home. createSession()/deleteSession() now swallow that specific failure --
the refreshed/cleared session is still correct for the rest of the current
request, it just won't persist when called from a context that can't write
cookies (the next request re-derives the same correct answer).
Also added requireSession(path) and wired it into every auth-required page
(cart, checkout, addresses, orders, order-groups/[uuid], profile) --
proxy.ts's gate is deliberately optimistic (cookie presence only, per
Next.js's own guidance), so a present-but-dead session was reaching these
pages and crashing the same way; they now redirect to /login instead.
Verified against the exact failure: a session with a dead access+refresh
token now renders the home page as logged-out (200, not 500) and redirects
/cart to /login (307) instead of crashing. Also verified the happy path
(a genuinely fresh, valid session from a real OTP login) still works.
Shows a clear/deselect row (only when a location is actually set) that
wipes the location cookie and returns to the unfiltered all-stores view.
Also highlights the currently selected neighborhood in the suggested/
results list with a checkmark, since it's now shown there too.
icon/image/logo/cover_image are now MinIO object keys, not URLs -- every
serializer with one of these fields now returns a <field>_url sibling
that's an already-absolute, freshly presigned URL. Added those fields to
every affected type (StoreCategory, StoreListItem, ProductCategory,
ProductListItem) and switched every <img> call site to use the _url field
directly.
Deleted resolveMediaUrl/resolveClientMediaUrl and the
NEXT_PUBLIC_WINOFY_MEDIA_ORIGIN env var -- they existed only to turn
relative paths under the Winofy origin into absolute URLs, which no longer
applies now that media lives on a different host (MinIO) and comes back
pre-resolved.
Also fixes cart-item-row.tsx's product thumbnail, which had been commented
out (broken under the old relative-path assumption).
Restored image_url as a required non-optional field per the doc rather
than treating it as possibly-missing, since every serializer covered by
this migration always includes it (null when no image, not omitted).
Implements winofyfrontenddoc (1).md §3-4: unified guest-home/authenticated-home
screen (header with login chip vs cart badge, pill search bar, dismissible
location-reminder banner, category chips, store cards, interleaved seller
promo card, sticky bottom nav), plus the full post-OTP location-onboarding
sub-flow (geolocation permission -> error/retry -> manual neighborhood
search, unified idle/results/empty states per the doc's own recommendation).
Location state persists in a plain (non-httpOnly) cookie readable both
client- and server-side, since it drives SSR store queries (neighborhood
or lat/lng) as well as client interactions.
Moved cart/checkout/store/product/orders/addresses under a new (shop) route
group so the new home-shell header doesn't double up with the existing
simple nav header on inner pages.
Real bug caught via live staging data: Neighborhood.city is a nested
{uuid, name, slug} object, not a bare uuid string as typed -- broke both
the address form's city filter and the new neighborhood search. Fixed and
verified against real API responses, not just assumed from the guide.