winofy-front/src/app/(customer)/location/search/page.tsx
Afra ‌ bf1dc0b346 feat: dockerize (multi-stage build, standalone output)
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.
2026-08-19 11:00:09 +03:30

23 lines
970 B
TypeScript

import { Suspense } from "react";
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
import { getLocationServer } from "@/lib/location/server";
import type { Neighborhood } from "@/types/api";
import { NeighborhoodSearch } from "@/components/location/neighborhood-search";
async function getNeighborhoods() {
const res = await winofyFetch<PaginatedResponse<Neighborhood> | Neighborhood[]>("/v1/neighborhoods/", {
auth: false,
params: { limit: 200 },
});
return Array.isArray(res) ? res : res.results;
}
export default async function NeighborhoodSearchPage() {
const [neighborhoods, location] = await Promise.all([getNeighborhoods(), getLocationServer()]);
const selectedNeighborhoodUuid = location?.mode === "manual" ? location.neighborhoodUuid : null;
return (
<Suspense fallback={null}>
<NeighborhoodSearch neighborhoods={neighborhoods} selectedNeighborhoodUuid={selectedNeighborhoodUuid} />
</Suspense>
);
}