Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions packages/nextjs/app/listing/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ const ListingDetailsPageInner = () => {
if (typeof (indexed as any)?.initialQuantity === "number") return (indexed as any).initialQuantity > 0;
return false;
}, [data?.decoded?.initialQuantity, indexed]);
const unlimited = useMemo(() => {
const init = data?.decoded?.initialQuantity as bigint | undefined;
if (typeof init === "bigint") return init === 0n;
if (typeof (indexed as any)?.unlimited === "boolean") return Boolean((indexed as any).unlimited);
return false;
}, [data?.decoded?.initialQuantity, indexed]);
const remaining = useMemo(() => {
const rem = data?.decoded?.remainingQuantity as bigint | undefined;
if (typeof rem === "bigint") return Number(rem);
Expand All @@ -263,11 +269,31 @@ const ListingDetailsPageInner = () => {
}, [data?.decoded?.remainingQuantity, indexed]);
const [quantity, setQuantity] = useState<number>(1);
useEffect(() => {
if (!limited) return;
const max = typeof remaining === "number" ? remaining : 1;
if (quantity > max) setQuantity(Math.max(1, max));
if (!limited || typeof remaining !== "number") return;
const max = remaining;
if (quantity > max) setQuantity(max > 0 ? max : 1);
}, [limited, remaining, quantity]);

const supportsQuantity = useMemo(
() => unlimited || (limited && typeof remaining === "number"),
[unlimited, limited, remaining],
);
useEffect(() => {
if (!supportsQuantity) {
setQuantity(1);
}
}, [supportsQuantity]);

// Disable buying and quantity input when listing is inactive or sold out
const buyDisabled = useMemo(
() => !active || (limited && typeof remaining === "number" && remaining < 1),
[active, limited, remaining],
);
const maxQuantity = useMemo(
() => (limited && typeof remaining === "number" ? remaining : undefined),
[limited, remaining],
);

const postedAgo = useMemo(() => {
const ts = indexed?.createdBlockTimestamp ? Number(indexed.createdBlockTimestamp) : undefined;
if (!ts) return null;
Expand Down Expand Up @@ -420,20 +446,21 @@ const ListingDetailsPageInner = () => {
<div className="badge badge-outline">{remaining} left</div>
) : null}
<div className="text-xl font-semibold">{priceLabel}</div>
{limited || true ? (
{supportsQuantity ? (
<div className="flex items-center gap-1">
<span className="opacity-70">x</span>
<input
className="input input-bordered input-sm w-16 text-center"
type="number"
min={1}
max={limited && typeof remaining === "number" ? Math.max(1, remaining) : undefined}
max={maxQuantity}
value={quantity}
disabled={buyDisabled || (typeof maxQuantity === "number" && maxQuantity < 1)}
onFocus={e => (e.target as HTMLInputElement).select()}
onClick={e => (e.currentTarget as HTMLInputElement).select()}
onChange={e => {
const v = Math.max(1, Number(e.target.value || "1"));
setQuantity(limited && typeof remaining === "number" ? Math.min(v, remaining) : v);
setQuantity(typeof maxQuantity === "number" ? Math.min(v, maxQuantity) : v);
}}
/>
</div>
Expand All @@ -446,7 +473,7 @@ const ListingDetailsPageInner = () => {
paymentToken={payToken}
listingTypeAddress={payListingTypeAddress}
quantity={quantity}
disabled={!active}
disabled={buyDisabled}
/>
) : null}
</div>
Expand Down
69 changes: 63 additions & 6 deletions packages/nextjs/app/location/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { ListingCard } from "~~/components/marketplace/ListingCard";
import MapRadius from "~~/components/marketplace/MapRadiusGL";

const LocationPage = () => {
const params = useParams<{ id: string }>();
Expand Down Expand Up @@ -63,6 +64,11 @@ const LocationPage = () => {
savedAt: Date.now(),
};
localStorage.setItem("marketplace.defaultLocationData", JSON.stringify(data));
// Mirror to cookie for server-side redirects (middleware)
try {
document.cookie =
"last_location_id=" + encodeURIComponent(String(decoded)) + "; Max-Age=15552000; Path=/; SameSite=Lax";
} catch {}
if (!cachedName && data.name) setCachedName(data.name);
}
} catch {}
Expand All @@ -78,6 +84,10 @@ const LocationPage = () => {
}
} catch {}
}
// Clear cookie if the stored id is invalid
try {
document.cookie = "last_location_id=; Max-Age=0; Path=/; SameSite=Lax";
} catch {}
} catch {}
// redirect to home if this location no longer exists
window.location.href = "/?home=1";
Expand Down Expand Up @@ -221,19 +231,26 @@ const LocationPage = () => {

return (
<div className="p-4 space-y-4">
<div className="mx-auto max-w-6xl flex items-center">
<h1 className="text-2xl mb-0 font-semibold">{location?.name || cachedName || ""}</h1>
<div className="flex items-center gap-2 flex-wrap">
<h1 className="text-2xl mb-0 font-semibold leading-tight">{location?.name || cachedName || ""}</h1>
<label
htmlFor="location-details-modal"
className="btn btn-secondary btn-sm ml-auto translate-y-[2px]"
aria-label="View location details"
>
Map
</label>
</div>

<div className="mx-auto max-w-6xl">
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search listings"
className="input input-bordered w-full"
/>
</div>
<div className="mx-auto max-w-6xl flex items-center justify-between gap-2">
<div className="flex items-center justify-between gap-2">
<details className="dropdown">
<summary className="btn btn-sm relative">
Filters
Expand Down Expand Up @@ -289,7 +306,7 @@ const LocationPage = () => {
</Link>
</div>
{loadingListings ? (
<div className="mx-auto max-w-6xl flex items-center justify-center py-8">
<div className="flex items-center justify-center py-8">
<span className="loading loading-spinner loading-md" />
<span className="ml-2 opacity-70">Loading listings…</span>
</div>
Expand All @@ -298,7 +315,7 @@ const LocationPage = () => {
<p className="opacity-70">No listings yet.</p>
</div>
) : (
<div className="mx-auto grid max-w-6xl grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filtered.map(item => (
<ListingCard
key={item.id}
Expand All @@ -313,6 +330,46 @@ const LocationPage = () => {
))}
</div>
)}
{/* Location Details Modal */}
<div>
<input type="checkbox" id="location-details-modal" className="modal-toggle" />
<label htmlFor="location-details-modal" className="modal cursor-pointer">
<label className="modal-box relative max-w-3xl max-h-[90vh] overflow-y-auto">
<input className="h-0 w-0 absolute top-0 left-0" />
<label htmlFor="location-details-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</label>
<div className="space-y-3">
<div className="text-lg font-semibold break-words">{location?.name || cachedName || "Location"}</div>
<div className="rounded-xl overflow-hidden border bg-base-100">
{location?.lat != null && location?.lng != null && location?.radiusMiles != null ? (
<MapRadius
lat={Number(location.lat)}
lng={Number(location.lng)}
radiusMiles={Number(location.radiusMiles)}
onMove={() => {}}
/>
) : (
<div className="p-4 text-sm opacity-70">No map preview available for this location.</div>
)}
</div>
{location && (
<div className="space-y-2">
{Array.isArray(location.akas) && location.akas.length > 0 && (
<div className="flex flex-wrap gap-2">
{location.akas.map((aka: string, idx: number) => (
<span key={idx} className="badge badge-outline max-w-[12rem]" title={aka}>
<span className="block max-w-full truncate text-left">{aka}</span>
</span>
))}
</div>
)}
</div>
)}
</div>
</label>
</label>
</div>
</div>
);
};
Expand Down
130 changes: 24 additions & 106 deletions packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Cormorant_Garamond } from "next/font/google";
import Image from "next/image";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import MapRadius from "~~/components/marketplace/MapRadiusGL";

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

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

const [query, setQuery] = useState("");
const [locations, setLocations] = useState<any[]>([]);
const [selected, setSelected] = useState<any | null>(null);
const [loadingSelected, setLoadingSelected] = useState(false);
const [loadingLocations, setLoadingLocations] = useState(true);

useEffect(() => {
Expand Down Expand Up @@ -58,33 +55,17 @@ const HomeInner = () => {
} catch {}
const id = parsed?.id as string | undefined;
if (id) {
// verify the location still exists before redirecting
fetch(`/api/locations/${id}`)
.then(async res => {
if (res.ok) {
router.replace(`/location/${id}`);
} else {
try {
localStorage.removeItem("marketplace.defaultLocationData");
} catch {}
router.replace(`/?home=1`);
}
})
.catch(() => {
try {
localStorage.removeItem("marketplace.defaultLocationData");
} catch {}
router.replace(`/?home=1`);
});
// Navigate immediately; the location page will validate and handle 404s/cleanup.
router.replace(`/location/${encodeURIComponent(id)}`);
}
}
} catch {}
}, [router, searchParams]);

return (
<>
<div className="flex items-center flex-col grow pt-2">
<div className="px-5 w-full max-w-5xl">
<div className="flex items-center justify-between gap-2 pt-2">
<div className="px-5 w-full">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="lg:hidden flex items-center gap-2">
Expand Down Expand Up @@ -132,22 +113,27 @@ const HomeInner = () => {
<button
key={l.id}
className="card bg-base-100 border border-base-300 hover:border-primary/60 transition-colors text-left cursor-pointer"
onClick={async () => {
setSelected(null);
setLoadingSelected(true);
onClick={() => {
// Set as preferred location immediately, then navigate
try {
const res = await fetch(`/api/locations/${encodeURIComponent(l.id)}`);
if (res.ok) {
const json = await res.json();
setSelected(json.location || l);
} else {
setSelected(l);
}
} finally {
setLoadingSelected(false);
const checkbox = document.getElementById("location-preview-modal") as HTMLInputElement | null;
if (checkbox) checkbox.checked = true;
}
const data = {
id: String(l.id),
name: l?.name ?? null,
lat: l?.lat ?? null,
lng: l?.lng ?? null,
radiusMiles: l?.radiusMiles ?? null,
savedAt: Date.now(),
};
localStorage.setItem("marketplace.defaultLocationData", JSON.stringify(data));
// Mirror to cookie for server-side redirects (middleware)
try {
document.cookie =
"last_location_id=" +
encodeURIComponent(String(l.id)) +
"; Max-Age=15552000; Path=/; SameSite=Lax";
} catch {}
} catch {}
router.push(`/location/${encodeURIComponent(l.id)}`);
}}
>
<div className="card-body p-3">
Expand All @@ -161,74 +147,6 @@ const HomeInner = () => {
</div>
</div>
</div>

{/* Location Preview Modal */}
<div>
<input type="checkbox" id="location-preview-modal" className="modal-toggle" />
<label htmlFor="location-preview-modal" className="modal cursor-pointer">
<label className="modal-box relative max-w-3xl max-h-[90vh] overflow-y-auto">
<input className="h-0 w-0 absolute top-0 left-0" />
<label htmlFor="location-preview-modal" className="btn btn-ghost btn-sm btn-circle absolute right-3 top-3">
</label>
<div className="space-y-3">
<div className="text-lg font-semibold">{selected?.name || selected?.id || "Location"}</div>
<div className="rounded-xl overflow-hidden border bg-base-100">
{loadingSelected ? (
<div className="p-4 text-sm opacity-70">Loading…</div>
) : selected?.lat != null && selected?.lng != null && selected?.radiusMiles != null ? (
<MapRadius
lat={Number(selected.lat)}
lng={Number(selected.lng)}
radiusMiles={Number(selected.radiusMiles)}
onMove={() => {}}
/>
) : (
<div className="p-4 text-sm opacity-70">No map preview available for this location.</div>
)}
</div>
{/* Additional location details */}
{selected && (
<div className="space-y-2">
{Array.isArray(selected.akas) && selected.akas.length > 0 && (
<div className="flex flex-wrap gap-2">
{selected.akas.map((aka: string, idx: number) => (
<span key={idx} className="badge badge-outline">
{aka}
</span>
))}
</div>
)}
</div>
)}
<button
className="btn btn-primary w-full"
onClick={() => {
try {
if (selected?.id) {
const data = {
id: String(selected.id),
name: selected?.name ?? null,
lat: selected?.lat ?? null,
lng: selected?.lng ?? null,
radiusMiles: selected?.radiusMiles ?? null,
savedAt: Date.now(),
};
localStorage.setItem("marketplace.defaultLocationData", JSON.stringify(data));
}
} catch {}
const checkbox = document.getElementById("location-preview-modal") as HTMLInputElement | null;
if (checkbox) checkbox.checked = false;
router.push(`/location/${String(selected?.id || "")}`);
}}
disabled={!selected}
>
Select this location
</button>
</div>
</label>
</label>
</div>
</>
);
};
Expand Down
4 changes: 3 additions & 1 deletion packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
<>
<div className={`flex flex-col min-h-screen `}>
<Header />
<main className="relative flex flex-col flex-1">{children}</main>
<main className="relative flex flex-col flex-1">
<div className="mx-auto max-w-6xl w-full">{children}</div>
</main>
{/* <Footer /> */}
</div>
<Toaster />
Expand Down
Loading