Skip to content

Commit 95f700a

Browse files
Refactor layout and improve quantity handling in listings. Updated page structure for better responsiveness and added logic to disable buying when listings are inactive or sold out.
1 parent 42a72f9 commit 95f700a

File tree

5 files changed

+154
-120
lines changed

5 files changed

+154
-120
lines changed

packages/nextjs/app/listing/[id]/page.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ const ListingDetailsPageInner = () => {
255255
if (typeof (indexed as any)?.initialQuantity === "number") return (indexed as any).initialQuantity > 0;
256256
return false;
257257
}, [data?.decoded?.initialQuantity, indexed]);
258+
const unlimited = useMemo(() => {
259+
const init = data?.decoded?.initialQuantity as bigint | undefined;
260+
if (typeof init === "bigint") return init === 0n;
261+
if (typeof (indexed as any)?.unlimited === "boolean") return Boolean((indexed as any).unlimited);
262+
return false;
263+
}, [data?.decoded?.initialQuantity, indexed]);
258264
const remaining = useMemo(() => {
259265
const rem = data?.decoded?.remainingQuantity as bigint | undefined;
260266
if (typeof rem === "bigint") return Number(rem);
@@ -263,11 +269,31 @@ const ListingDetailsPageInner = () => {
263269
}, [data?.decoded?.remainingQuantity, indexed]);
264270
const [quantity, setQuantity] = useState<number>(1);
265271
useEffect(() => {
266-
if (!limited) return;
267-
const max = typeof remaining === "number" ? remaining : 1;
268-
if (quantity > max) setQuantity(Math.max(1, max));
272+
if (!limited || typeof remaining !== "number") return;
273+
const max = remaining;
274+
if (quantity > max) setQuantity(max > 0 ? max : 1);
269275
}, [limited, remaining, quantity]);
270276

277+
const supportsQuantity = useMemo(
278+
() => unlimited || (limited && typeof remaining === "number"),
279+
[unlimited, limited, remaining],
280+
);
281+
useEffect(() => {
282+
if (!supportsQuantity) {
283+
setQuantity(1);
284+
}
285+
}, [supportsQuantity]);
286+
287+
// Disable buying and quantity input when listing is inactive or sold out
288+
const buyDisabled = useMemo(
289+
() => !active || (limited && typeof remaining === "number" && remaining < 1),
290+
[active, limited, remaining],
291+
);
292+
const maxQuantity = useMemo(
293+
() => (limited && typeof remaining === "number" ? remaining : undefined),
294+
[limited, remaining],
295+
);
296+
271297
const postedAgo = useMemo(() => {
272298
const ts = indexed?.createdBlockTimestamp ? Number(indexed.createdBlockTimestamp) : undefined;
273299
if (!ts) return null;
@@ -420,20 +446,21 @@ const ListingDetailsPageInner = () => {
420446
<div className="badge badge-outline">{remaining} left</div>
421447
) : null}
422448
<div className="text-xl font-semibold">{priceLabel}</div>
423-
{limited || true ? (
449+
{supportsQuantity ? (
424450
<div className="flex items-center gap-1">
425451
<span className="opacity-70">x</span>
426452
<input
427453
className="input input-bordered input-sm w-16 text-center"
428454
type="number"
429455
min={1}
430-
max={limited && typeof remaining === "number" ? Math.max(1, remaining) : undefined}
456+
max={maxQuantity}
431457
value={quantity}
458+
disabled={buyDisabled || (typeof maxQuantity === "number" && maxQuantity < 1)}
432459
onFocus={e => (e.target as HTMLInputElement).select()}
433460
onClick={e => (e.currentTarget as HTMLInputElement).select()}
434461
onChange={e => {
435462
const v = Math.max(1, Number(e.target.value || "1"));
436-
setQuantity(limited && typeof remaining === "number" ? Math.min(v, remaining) : v);
463+
setQuantity(typeof maxQuantity === "number" ? Math.min(v, maxQuantity) : v);
437464
}}
438465
/>
439466
</div>
@@ -446,7 +473,7 @@ const ListingDetailsPageInner = () => {
446473
paymentToken={payToken}
447474
listingTypeAddress={payListingTypeAddress}
448475
quantity={quantity}
449-
disabled={!active}
476+
disabled={buyDisabled}
450477
/>
451478
) : null}
452479
</div>

packages/nextjs/app/location/[id]/page.tsx

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import Link from "next/link";
55
import { useParams } from "next/navigation";
66
import { ListingCard } from "~~/components/marketplace/ListingCard";
7+
import MapRadius from "~~/components/marketplace/MapRadiusGL";
78

89
const LocationPage = () => {
910
const params = useParams<{ id: string }>();
@@ -63,6 +64,11 @@ const LocationPage = () => {
6364
savedAt: Date.now(),
6465
};
6566
localStorage.setItem("marketplace.defaultLocationData", JSON.stringify(data));
67+
// Mirror to cookie for server-side redirects (middleware)
68+
try {
69+
document.cookie =
70+
"last_location_id=" + encodeURIComponent(String(decoded)) + "; Max-Age=15552000; Path=/; SameSite=Lax";
71+
} catch {}
6672
if (!cachedName && data.name) setCachedName(data.name);
6773
}
6874
} catch {}
@@ -78,6 +84,10 @@ const LocationPage = () => {
7884
}
7985
} catch {}
8086
}
87+
// Clear cookie if the stored id is invalid
88+
try {
89+
document.cookie = "last_location_id=; Max-Age=0; Path=/; SameSite=Lax";
90+
} catch {}
8191
} catch {}
8292
// redirect to home if this location no longer exists
8393
window.location.href = "/?home=1";
@@ -221,19 +231,26 @@ const LocationPage = () => {
221231

222232
return (
223233
<div className="p-4 space-y-4">
224-
<div className="mx-auto max-w-6xl flex items-center">
225-
<h1 className="text-2xl mb-0 font-semibold">{location?.name || cachedName || ""}</h1>
234+
<div className="flex items-center gap-2 flex-wrap">
235+
<h1 className="text-2xl mb-0 font-semibold leading-tight">{location?.name || cachedName || ""}</h1>
236+
<label
237+
htmlFor="location-details-modal"
238+
className="btn btn-secondary btn-sm ml-auto translate-y-[2px]"
239+
aria-label="View location details"
240+
>
241+
Map
242+
</label>
226243
</div>
227244

228-
<div className="mx-auto max-w-6xl">
245+
<div>
229246
<input
230247
value={query}
231248
onChange={e => setQuery(e.target.value)}
232249
placeholder="Search listings"
233250
className="input input-bordered w-full"
234251
/>
235252
</div>
236-
<div className="mx-auto max-w-6xl flex items-center justify-between gap-2">
253+
<div className="flex items-center justify-between gap-2">
237254
<details className="dropdown">
238255
<summary className="btn btn-sm relative">
239256
Filters
@@ -289,7 +306,7 @@ const LocationPage = () => {
289306
</Link>
290307
</div>
291308
{loadingListings ? (
292-
<div className="mx-auto max-w-6xl flex items-center justify-center py-8">
309+
<div className="flex items-center justify-center py-8">
293310
<span className="loading loading-spinner loading-md" />
294311
<span className="ml-2 opacity-70">Loading listings…</span>
295312
</div>
@@ -298,7 +315,7 @@ const LocationPage = () => {
298315
<p className="opacity-70">No listings yet.</p>
299316
</div>
300317
) : (
301-
<div className="mx-auto grid max-w-6xl grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
318+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
302319
{filtered.map(item => (
303320
<ListingCard
304321
key={item.id}
@@ -313,6 +330,46 @@ const LocationPage = () => {
313330
))}
314331
</div>
315332
)}
333+
{/* Location Details Modal */}
334+
<div>
335+
<input type="checkbox" id="location-details-modal" className="modal-toggle" />
336+
<label htmlFor="location-details-modal" className="modal cursor-pointer">
337+
<label className="modal-box relative max-w-3xl max-h-[90vh] overflow-y-auto">
338+
<input className="h-0 w-0 absolute top-0 left-0" />
339+
<label htmlFor="location-details-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
340+
341+
</label>
342+
<div className="space-y-3">
343+
<div className="text-lg font-semibold break-words">{location?.name || cachedName || "Location"}</div>
344+
<div className="rounded-xl overflow-hidden border bg-base-100">
345+
{location?.lat != null && location?.lng != null && location?.radiusMiles != null ? (
346+
<MapRadius
347+
lat={Number(location.lat)}
348+
lng={Number(location.lng)}
349+
radiusMiles={Number(location.radiusMiles)}
350+
onMove={() => {}}
351+
/>
352+
) : (
353+
<div className="p-4 text-sm opacity-70">No map preview available for this location.</div>
354+
)}
355+
</div>
356+
{location && (
357+
<div className="space-y-2">
358+
{Array.isArray(location.akas) && location.akas.length > 0 && (
359+
<div className="flex flex-wrap gap-2">
360+
{location.akas.map((aka: string, idx: number) => (
361+
<span key={idx} className="badge badge-outline max-w-[12rem]" title={aka}>
362+
<span className="block max-w-full truncate text-left">{aka}</span>
363+
</span>
364+
))}
365+
</div>
366+
)}
367+
</div>
368+
)}
369+
</div>
370+
</label>
371+
</label>
372+
</div>
316373
</div>
317374
);
318375
};

packages/nextjs/app/page.tsx

Lines changed: 30 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Cormorant_Garamond } from "next/font/google";
55
import Image from "next/image";
66
import Link from "next/link";
77
import { useRouter, useSearchParams } from "next/navigation";
8-
import MapRadius from "~~/components/marketplace/MapRadiusGL";
98

109
const cormorant = Cormorant_Garamond({ subsets: ["latin"], weight: ["500", "600", "700"] });
1110

@@ -15,8 +14,6 @@ const HomeInner = () => {
1514

1615
const [query, setQuery] = useState("");
1716
const [locations, setLocations] = useState<any[]>([]);
18-
const [selected, setSelected] = useState<any | null>(null);
19-
const [loadingSelected, setLoadingSelected] = useState(false);
2017
const [loadingLocations, setLoadingLocations] = useState(true);
2118

2219
useEffect(() => {
@@ -58,33 +55,17 @@ const HomeInner = () => {
5855
} catch {}
5956
const id = parsed?.id as string | undefined;
6057
if (id) {
61-
// verify the location still exists before redirecting
62-
fetch(`/api/locations/${id}`)
63-
.then(async res => {
64-
if (res.ok) {
65-
router.replace(`/location/${id}`);
66-
} else {
67-
try {
68-
localStorage.removeItem("marketplace.defaultLocationData");
69-
} catch {}
70-
router.replace(`/?home=1`);
71-
}
72-
})
73-
.catch(() => {
74-
try {
75-
localStorage.removeItem("marketplace.defaultLocationData");
76-
} catch {}
77-
router.replace(`/?home=1`);
78-
});
58+
// Navigate immediately; the location page will validate and handle 404s/cleanup.
59+
router.replace(`/location/${encodeURIComponent(id)}`);
7960
}
8061
}
8162
} catch {}
8263
}, [router, searchParams]);
8364

8465
return (
8566
<>
86-
<div className="flex items-center flex-col grow pt-2">
87-
<div className="px-5 w-full max-w-5xl">
67+
<div className="flex items-center justify-between gap-2 pt-2">
68+
<div className="px-5 w-full">
8869
<div className="flex items-center justify-between">
8970
<div className="flex-1">
9071
<div className="lg:hidden flex items-center gap-2">
@@ -132,22 +113,27 @@ const HomeInner = () => {
132113
<button
133114
key={l.id}
134115
className="card bg-base-100 border border-base-300 hover:border-primary/60 transition-colors text-left cursor-pointer"
135-
onClick={async () => {
136-
setSelected(null);
137-
setLoadingSelected(true);
116+
onClick={() => {
117+
// Set as preferred location immediately, then navigate
138118
try {
139-
const res = await fetch(`/api/locations/${encodeURIComponent(l.id)}`);
140-
if (res.ok) {
141-
const json = await res.json();
142-
setSelected(json.location || l);
143-
} else {
144-
setSelected(l);
145-
}
146-
} finally {
147-
setLoadingSelected(false);
148-
const checkbox = document.getElementById("location-preview-modal") as HTMLInputElement | null;
149-
if (checkbox) checkbox.checked = true;
150-
}
119+
const data = {
120+
id: String(l.id),
121+
name: l?.name ?? null,
122+
lat: l?.lat ?? null,
123+
lng: l?.lng ?? null,
124+
radiusMiles: l?.radiusMiles ?? null,
125+
savedAt: Date.now(),
126+
};
127+
localStorage.setItem("marketplace.defaultLocationData", JSON.stringify(data));
128+
// Mirror to cookie for server-side redirects (middleware)
129+
try {
130+
document.cookie =
131+
"last_location_id=" +
132+
encodeURIComponent(String(l.id)) +
133+
"; Max-Age=15552000; Path=/; SameSite=Lax";
134+
} catch {}
135+
} catch {}
136+
router.push(`/location/${encodeURIComponent(l.id)}`);
151137
}}
152138
>
153139
<div className="card-body p-3">
@@ -161,81 +147,19 @@ const HomeInner = () => {
161147
</div>
162148
</div>
163149
</div>
164-
165-
{/* Location Preview Modal */}
166-
<div>
167-
<input type="checkbox" id="location-preview-modal" className="modal-toggle" />
168-
<label htmlFor="location-preview-modal" className="modal cursor-pointer">
169-
<label className="modal-box relative max-w-3xl max-h-[90vh] overflow-y-auto">
170-
<input className="h-0 w-0 absolute top-0 left-0" />
171-
<label htmlFor="location-preview-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
172-
173-
</label>
174-
<div className="space-y-3">
175-
<div className="text-lg font-semibold">{selected?.name || selected?.id || "Location"}</div>
176-
<div className="rounded-xl overflow-hidden border bg-base-100">
177-
{loadingSelected ? (
178-
<div className="p-4 text-sm opacity-70">Loading…</div>
179-
) : selected?.lat != null && selected?.lng != null && selected?.radiusMiles != null ? (
180-
<MapRadius
181-
lat={Number(selected.lat)}
182-
lng={Number(selected.lng)}
183-
radiusMiles={Number(selected.radiusMiles)}
184-
onMove={() => {}}
185-
/>
186-
) : (
187-
<div className="p-4 text-sm opacity-70">No map preview available for this location.</div>
188-
)}
189-
</div>
190-
{/* Additional location details */}
191-
{selected && (
192-
<div className="space-y-2">
193-
{Array.isArray(selected.akas) && selected.akas.length > 0 && (
194-
<div className="flex flex-wrap gap-2">
195-
{selected.akas.map((aka: string, idx: number) => (
196-
<span key={idx} className="badge badge-outline">
197-
{aka}
198-
</span>
199-
))}
200-
</div>
201-
)}
202-
</div>
203-
)}
204-
<button
205-
className="btn btn-primary w-full"
206-
onClick={() => {
207-
try {
208-
if (selected?.id) {
209-
const data = {
210-
id: String(selected.id),
211-
name: selected?.name ?? null,
212-
lat: selected?.lat ?? null,
213-
lng: selected?.lng ?? null,
214-
radiusMiles: selected?.radiusMiles ?? null,
215-
savedAt: Date.now(),
216-
};
217-
localStorage.setItem("marketplace.defaultLocationData", JSON.stringify(data));
218-
}
219-
} catch {}
220-
const checkbox = document.getElementById("location-preview-modal") as HTMLInputElement | null;
221-
if (checkbox) checkbox.checked = false;
222-
router.push(`/location/${String(selected?.id || "")}`);
223-
}}
224-
disabled={!selected}
225-
>
226-
Select this location
227-
</button>
228-
</div>
229-
</label>
230-
</label>
231-
</div>
232150
</>
233151
);
234152
};
235153

236154
export default function Home() {
237155
return (
238156
<Suspense fallback={null}>
157+
{/* Early redirect before hydration using localStorage (no data fetch) */}
158+
<script
159+
dangerouslySetInnerHTML={{
160+
__html: `(function(){try{var params=new URLSearchParams(window.location.search);if(params.get('home')==='1')return;var raw=localStorage.getItem('marketplace.defaultLocationData');if(!raw)return;var parsed;try{parsed=JSON.parse(raw)}catch(e){parsed=null}var id=parsed&&parsed.id;if(id){window.location.replace('/location/'+encodeURIComponent(id))}}catch(e){}})();`,
161+
}}
162+
/>
239163
<HomeInner />
240164
</Suspense>
241165
);

0 commit comments

Comments
 (0)