Skip to content

Commit 6b381f9

Browse files
Merge pull request #6 from BuidlGuidl/ui-changes
Refactor layout and improve quantity handling in listings
2 parents 42a72f9 + e856508 commit 6b381f9

File tree

5 files changed

+148
-120
lines changed

5 files changed

+148
-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: 24 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,74 +147,6 @@ 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
};

packages/nextjs/components/ScaffoldEthAppWithProviders.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
2121
<>
2222
<div className={`flex flex-col min-h-screen `}>
2323
<Header />
24-
<main className="relative flex flex-col flex-1">{children}</main>
24+
<main className="relative flex flex-col flex-1">
25+
<div className="mx-auto max-w-6xl w-full">{children}</div>
26+
</main>
2527
{/* <Footer /> */}
2628
</div>
2729
<Toaster />

0 commit comments

Comments
 (0)