|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 3 | +import { useEffect, useMemo, useState } from "react"; |
4 | 4 | import Link from "next/link"; |
5 | 5 | import { useParams } from "next/navigation"; |
6 | 6 | import { ListingCard } from "~~/components/marketplace/ListingCard"; |
7 | 7 | import MapRadius from "~~/components/marketplace/MapRadiusGL"; |
| 8 | +import { useListingsByLocation } from "~~/hooks/marketplace/useListingsByLocation"; |
8 | 9 |
|
9 | 10 | const LocationPage = () => { |
10 | 11 | const params = useParams<{ id: string }>(); |
11 | 12 | const [query, setQuery] = useState(""); |
12 | | - const [listings, setListings] = useState<any[]>([]); |
13 | | - const [loadingListings, setLoadingListings] = useState(true); |
14 | 13 | const [location, setLocation] = useState<any | null>(null); |
15 | 14 | const [cachedName, setCachedName] = useState<string | null>(null); |
16 | 15 | const [tagFilter, setTagFilter] = useState<string>(""); |
17 | 16 |
|
18 | | - const refreshDelay = process.env.NEXT_PUBLIC_REFRESH_DELAY ? Number(process.env.NEXT_PUBLIC_REFRESH_DELAY) : 4000; |
19 | | - // Future: we could store geo/radius for map previews |
20 | | - const hasRefreshedRef = useRef(false); |
| 17 | + const locationId = params?.id ? decodeURIComponent(params.id) : null; |
| 18 | + const { data: listings = [], isLoading: loadingListings } = useListingsByLocation(locationId); |
21 | 19 |
|
22 | 20 | useEffect(() => { |
23 | 21 | const run = async () => { |
@@ -96,111 +94,6 @@ const LocationPage = () => { |
96 | 94 | run(); |
97 | 95 | }, [params?.id, cachedName]); |
98 | 96 |
|
99 | | - // Fetch listings for this location from Ponder GraphQL |
100 | | - const fetchListings = useCallback( |
101 | | - async (opts?: { silent?: boolean }) => { |
102 | | - const id = decodeURIComponent(params?.id as string); |
103 | | - if (!id) return; |
104 | | - if (!opts?.silent) setLoadingListings(true); |
105 | | - try { |
106 | | - const res = await fetch(process.env.NEXT_PUBLIC_PONDER_URL || "http://localhost:42069/graphql", { |
107 | | - method: "POST", |
108 | | - headers: { "content-type": "application/json" }, |
109 | | - body: JSON.stringify({ |
110 | | - query: ` |
111 | | - query ListingsByLocation($loc: String!) { |
112 | | - listingss( |
113 | | - where: { locationId: $loc, active: true } |
114 | | - orderBy: "createdBlockNumber" |
115 | | - orderDirection: "desc" |
116 | | - limit: 100 |
117 | | - ) { |
118 | | - items { |
119 | | - id |
120 | | - title |
121 | | - image |
122 | | - tags |
123 | | - priceWei |
124 | | - tokenSymbol |
125 | | - tokenDecimals |
126 | | - } |
127 | | - } |
128 | | - }`, |
129 | | - variables: { loc: id }, |
130 | | - }), |
131 | | - }); |
132 | | - const json = await res.json(); |
133 | | - const items = (json?.data?.listingss?.items || []).map((it: any) => { |
134 | | - let tags: string[] = []; |
135 | | - try { |
136 | | - const raw = (it as any)?.tags; |
137 | | - if (Array.isArray(raw)) { |
138 | | - tags = raw.map((t: any) => String(t)).filter(Boolean); |
139 | | - } else if (typeof raw === "string" && raw) { |
140 | | - try { |
141 | | - const parsed = JSON.parse(raw); |
142 | | - if (Array.isArray(parsed)) tags = parsed.map((t: any) => String(t)).filter(Boolean); |
143 | | - } catch {} |
144 | | - } |
145 | | - } catch {} |
146 | | - return { |
147 | | - id: it.id, |
148 | | - title: it?.title ?? it.id, |
149 | | - image: it?.image ?? null, |
150 | | - tags, |
151 | | - priceWei: it?.priceWei ?? null, |
152 | | - tokenSymbol: it?.tokenSymbol ?? null, |
153 | | - tokenDecimals: it?.tokenDecimals ?? null, |
154 | | - }; |
155 | | - }); |
156 | | - setListings(items); |
157 | | - } catch { |
158 | | - setListings([]); |
159 | | - } finally { |
160 | | - if (!opts?.silent) setLoadingListings(false); |
161 | | - } |
162 | | - }, |
163 | | - [params?.id], |
164 | | - ); |
165 | | - |
166 | | - useEffect(() => { |
167 | | - fetchListings(); |
168 | | - }, [fetchListings]); |
169 | | - |
170 | | - // One-time delayed refresh ~Xs after landing |
171 | | - useEffect(() => { |
172 | | - hasRefreshedRef.current = false; |
173 | | - const timeout = setTimeout(() => { |
174 | | - if (!hasRefreshedRef.current) { |
175 | | - hasRefreshedRef.current = true; |
176 | | - const doc = document.documentElement; |
177 | | - const wasAtTop = window.scrollY === 0; |
178 | | - const prevFromBottom = Math.max(0, doc.scrollHeight - window.scrollY - window.innerHeight); |
179 | | - fetchListings({ silent: true }).finally(() => { |
180 | | - // Restore position relative to bottom to account for new content height |
181 | | - // Double rAF to ensure layout has settled after state updates |
182 | | - requestAnimationFrame(() => { |
183 | | - requestAnimationFrame(() => { |
184 | | - if (wasAtTop) { |
185 | | - return; |
186 | | - } |
187 | | - const newDoc = document.documentElement; |
188 | | - const targetTop = Math.max( |
189 | | - 0, |
190 | | - Math.min( |
191 | | - newDoc.scrollHeight - window.innerHeight, |
192 | | - newDoc.scrollHeight - prevFromBottom - window.innerHeight, |
193 | | - ), |
194 | | - ); |
195 | | - window.scrollTo({ top: targetTop, left: 0, behavior: "auto" }); |
196 | | - }); |
197 | | - }); |
198 | | - }); |
199 | | - } |
200 | | - }, refreshDelay); |
201 | | - return () => clearTimeout(timeout); |
202 | | - }, [fetchListings, params?.id, refreshDelay]); |
203 | | - |
204 | 97 | const availableTags = useMemo(() => { |
205 | 98 | const tagSet = listings.reduce((acc: Set<string>, listing) => { |
206 | 99 | if (Array.isArray(listing.tags)) { |
@@ -301,7 +194,7 @@ const LocationPage = () => { |
301 | 194 | </div> |
302 | 195 | </div> |
303 | 196 | </details> |
304 | | - <Link href={`/listing/new?loc=${encodeURIComponent(params?.id as string)}`} className="btn btn-sm btn-primary"> |
| 197 | + <Link href={`/listing/new?loc=${encodeURIComponent(locationId || "")}`} className="btn btn-sm btn-primary"> |
305 | 198 | Create Listing |
306 | 199 | </Link> |
307 | 200 | </div> |
|
0 commit comments