winofy-front/src/app/(customer)/location/permission/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

87 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Suspense, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { MapPin, Zap, Store } from "lucide-react";
import { Button } from "@/components/ui/button";
import { setLocation } from "@/lib/location/client";
const BENEFITS = [
{ icon: MapPin, text: "نمایش فروشگاه‌های محله شما" },
{ icon: Zap, text: "تحویل سریع‌تر" },
{ icon: Store, text: "کشف بازارچه‌های اطراف" },
];
// L01 — winofyfrontenddoc (1).md §4.1. No illustration asset was provided;
// approximated with a simple icon badge rather than the isometric map graphic.
export default function LocationPermissionPage() {
return (
<Suspense fallback={null}>
<LocationPermissionPageInner />
</Suspense>
);
}
function LocationPermissionPageInner() {
const router = useRouter();
const searchParams = useSearchParams();
const next = searchParams.get("next") ?? "/";
const [loading, setLoading] = useState(false);
function allowLocation() {
setLoading(true);
navigator.geolocation.getCurrentPosition(
(position) => {
setLocation({ mode: "geo", lat: position.coords.latitude, lng: position.coords.longitude });
router.push(next);
router.refresh();
},
() => {
setLoading(false);
router.push(`/location/error?next=${encodeURIComponent(next)}`);
},
{ timeout: 10000 },
);
}
return (
<main className="flex min-h-full flex-1 flex-col items-center gap-8 bg-surface px-6 py-10 text-center">
<span className="flex h-28 w-28 items-center justify-center rounded-full bg-primary-action/10 text-primary-action">
<MapPin className="h-14 w-14" />
</span>
<div className="flex flex-col gap-2">
<h1 className="text-headline-medium font-extrabold text-foreground">بازارچه محلی شما</h1>
<p className="text-body-large text-gray-700">برای نمایش فروشگاه‌های نزدیک، به موقعیت مکانی شما نیاز داریم</p>
</div>
<div className="flex w-full flex-col gap-3 text-right">
{BENEFITS.map(({ icon: Icon, text }) => (
<div key={text} className="flex items-center gap-3 rounded-xl border border-gray-300 bg-white p-3">
<Icon className="h-5 w-5 shrink-0 text-primary-action" />
<span className="text-body-medium text-foreground">{text}</span>
</div>
))}
</div>
<div className="mt-auto flex w-full flex-col gap-4">
<Button
size="lg"
className="w-full bg-primary-action hover:bg-primary-action/90 text-primary-foreground"
disabled={loading}
onClick={allowLocation}
>
{loading ? "در حال دریافت موقعیت..." : "اجازه دسترسی به موقعیت مکانی"}
</Button>
<button
type="button"
onClick={() => router.push(`/location/search?next=${encodeURIComponent(next)}`)}
className="text-body-medium font-semibold text-primary-action"
>
انتخاب دستی محله
</button>
<span className="text-body-medium text-gray-500">پشتیبانی</span>
</div>
</main>
);
}