From 069d80ec7ac41c5ca3bd16048ccee1a83fe997c4 Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 28 Apr 2026 02:52:34 +0100 Subject: [PATCH 1/5] feat: implement advanced filters for artisan search --- backend/app/api/v1/endpoints/artisan.py | 12 +- backend/app/schemas/artisan.py | 6 + backend/app/services/artisan.py | 6 + backend/app/services/artisan_service.py | 2 + frontend/app/artisans/page.tsx | 404 ++++++++++++++---------- frontend/lib/api.ts | 8 +- 6 files changed, 258 insertions(+), 180 deletions(-) diff --git a/backend/app/api/v1/endpoints/artisan.py b/backend/app/api/v1/endpoints/artisan.py index baa0bec..72ba976 100644 --- a/backend/app/api/v1/endpoints/artisan.py +++ b/backend/app/api/v1/endpoints/artisan.py @@ -81,10 +81,14 @@ async def get_nearby_artisans( radius_km: float = Query( 25.0, ge=0, le=200, description="Search radius in kilometers" ), - skill: str - | None = Query(None, description="Filter by skill keyword (e.g., plumber)"), + specialties: list[str] + | None = Query(None, description="Filter by multiple skill keywords"), min_rating: float | None = Query(None, ge=0, le=5, description="Minimum average rating"), + max_price: float + | None = Query(None, ge=0, description="Maximum hourly rate"), + min_experience: int + | None = Query(None, ge=0, description="Minimum years of experience"), available: bool | None = Query(None, description="Filter by current availability"), page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), @@ -97,8 +101,10 @@ async def get_nearby_artisans( latitude=lat, longitude=lon, radius_km=radius_km, - specialties=[skill] if skill else None, + specialties=specialties, min_rating=min_rating, + max_price=max_price, + min_experience=min_experience, is_available=available if available is not None else True, limit=page_size * page, # Fetch enough for pagination ) diff --git a/backend/app/schemas/artisan.py b/backend/app/schemas/artisan.py index 9f3353d..e9e5a95 100644 --- a/backend/app/schemas/artisan.py +++ b/backend/app/schemas/artisan.py @@ -166,6 +166,12 @@ class NearbyArtisansRequest(BaseModel): min_rating: float | None = Field( None, ge=0, le=5, description="Minimum rating filter" ) + max_price: float | None = Field( + None, ge=0, description="Maximum hourly rate filter" + ) + min_experience: int | None = Field( + None, ge=0, description="Minimum experience years filter" + ) is_available: bool | None = Field(True, description="Filter by availability") limit: int | None = Field(20, ge=1, le=100, description="Maximum number of results") diff --git a/backend/app/services/artisan.py b/backend/app/services/artisan.py index e78f83d..260a73e 100644 --- a/backend/app/services/artisan.py +++ b/backend/app/services/artisan.py @@ -231,6 +231,12 @@ async def find_nearby_artisans(self, request: NearbyArtisansRequest) -> dict: if request.min_rating is not None: query = query.filter(Artisan.rating >= request.min_rating) + if request.max_price is not None: + query = query.filter(Artisan.hourly_rate <= request.max_price) + + if request.min_experience is not None: + query = query.filter(Artisan.experience_years >= request.min_experience) + if request.is_available is not None: query = query.filter(Artisan.is_available == request.is_available) diff --git a/backend/app/services/artisan_service.py b/backend/app/services/artisan_service.py index c1587bd..4d72365 100644 --- a/backend/app/services/artisan_service.py +++ b/backend/app/services/artisan_service.py @@ -19,6 +19,8 @@ def _build_cache_key(request: NearbyArtisansRequest) -> str: "radius": request.radius_km, "specialties": sorted(request.specialties or []), # order-independent "min_rating": request.min_rating, + "max_price": request.max_price, + "min_experience": request.min_experience, "available": request.is_available, "limit": request.limit, } diff --git a/frontend/app/artisans/page.tsx b/frontend/app/artisans/page.tsx index 7f5aad9..27bb373 100644 --- a/frontend/app/artisans/page.tsx +++ b/frontend/app/artisans/page.tsx @@ -193,20 +193,22 @@ export default function ArtisansPage() { const pageSize = 12; // Filters - const [skill, setSkill] = useState(""); + const [specialties, setSpecialties] = useState([]); const [minRating, setMinRating] = useState(0); + const [maxPrice, setMaxPrice] = useState(""); + const [minExperience, setMinExperience] = useState(""); const [isAvailable, setIsAvailable] = useState(false); // Debounced filters - const [debouncedFilters, setDebouncedFilters] = useState({ skill, minRating, isAvailable }); + const [debouncedFilters, setDebouncedFilters] = useState({ specialties, minRating, maxPrice, minExperience, isAvailable }); useEffect(() => { const handler = setTimeout(() => { - setDebouncedFilters({ skill, minRating, isAvailable }); + setDebouncedFilters({ specialties, minRating, maxPrice, minExperience, isAvailable }); setPage(1); // reset to page 1 on filter }, 500); return () => clearTimeout(handler); - }, [skill, minRating, isAvailable]); + }, [specialties, minRating, maxPrice, minExperience, isAvailable]); const requestLocation = () => { if (typeof window === "undefined" || !navigator.geolocation) { @@ -244,8 +246,10 @@ export default function ArtisansPage() { .nearby(lat, lon, { page, page_size: pageSize, - skill: debouncedFilters.skill || undefined, + specialties: debouncedFilters.specialties.length > 0 ? debouncedFilters.specialties : undefined, min_rating: debouncedFilters.minRating || undefined, + max_price: debouncedFilters.maxPrice !== "" ? Number(debouncedFilters.maxPrice) : undefined, + min_experience: debouncedFilters.minExperience !== "" ? Number(debouncedFilters.minExperience) : undefined, is_available: debouncedFilters.isAvailable ? true : undefined, }) .then((res) => { @@ -267,8 +271,10 @@ export default function ArtisansPage() { }, [lat, lon, page, debouncedFilters]); const clearFilters = () => { - setSkill(""); + setSpecialties([]); setMinRating(0); + setMaxPrice(""); + setMinExperience(""); setIsAvailable(false); }; @@ -293,184 +299,232 @@ export default function ArtisansPage() { )} - {/* Filter Bar */} -
-
- - -
-
- - setMinRating(Number(e.target.value))} - className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" - /> -
-
- setIsAvailable(e.target.checked)} - className="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" - /> - -
-
+ {/* Filter Sidebar & Main Content Layout */} +
+ {/* Sidebar */} + - {showInitialSkeleton ? ( -
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
- ) : artisans.length === 0 ? ( -
- -

No artisans found

-

Try adjusting your filters to find what you're looking for.

- -
- ) : ( - <> -
-
- {artisans.map((a) => ( - - - -
-
-
- -
-
-

- {a.business_name || specialtyLabel(a)} -

-

- - {a.location || "Location not set"} -

-
- {a.rating != null && ( -

- - {Number(a.rating).toFixed(1)} -

- )} - {a.hourly_rate != null && ( -

- /hr + {/* Main Content */} +

+ {error && ( +

{error}

+ )} + +
+
+
+
+

+ Search Results +

+

+ {showInitialSkeleton ? "Finding artisans near you" : `${total} artisans available`} +

+

+ {loading + ? hasLoadedResults + ? "Updating the list with your latest filters." + : "Loading cards and map markers for this area." + : "Browse detailed cards while the map keeps nearby context in view."} +

+
+
+ {locationStatus === "granted" ? "Live location" : "Fallback location"} +
+
+
+ +
+ + {showInitialSkeleton ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : artisans.length === 0 ? ( +
+ +

No artisans found

+

Try adjusting your filters to find what you're looking for.

+ +
+ ) : ( + <> +
+
+ {artisans.map((a) => ( + + + +
+
+
+ +
+
+

+ {a.business_name || specialtyLabel(a)} +

+

+ + {a.location || "Location not set"}

- )} +
+ {a.rating != null && ( +

+ + {Number(a.rating).toFixed(1)} +

+ )} + {a.hourly_rate != null && ( +

+ /hr +

+ )} +
+
-
-
-
- {a.is_available && ( - - Available now - - )} - {a.distance_km != null && ( - - {Number(a.distance_km).toFixed(1)} km away - - )} -
-
- - - - ))} -
- {loading && ( -
-
-
-
- )} -
- {total > pageSize && ( -
- -
- Page {page} of {Math.ceil(total / pageSize)} +
+ {a.is_available && ( + + Available now + + )} + {a.distance_km != null && ( + + {Number(a.distance_km).toFixed(1)} km away + + )} +
+
+ + + + ))} +
+ {loading && ( +
+
+
+
+ )}
- -
+ {total > pageSize && ( +
+ +
+ Page {page} of {Math.ceil(total / pageSize)} +
+ +
+ )} + )} - - )} +
+
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 98fa02c..dd242d0 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -149,7 +149,7 @@ export const api = { nearby: ( lat: number, lon: number, - opts: { page?: number; page_size?: number; skill?: string; min_rating?: number; is_available?: boolean } = {} + opts: { page?: number; page_size?: number; specialties?: string[]; min_rating?: number; max_price?: number; min_experience?: number; is_available?: boolean } = {} ) => { const params = new URLSearchParams({ lat: String(lat), @@ -157,8 +157,12 @@ export const api = { page: String(opts.page ?? 1), page_size: String(opts.page_size ?? 10), }); - if (opts.skill) params.append("skill", opts.skill); + if (opts.specialties && opts.specialties.length > 0) { + opts.specialties.forEach(spec => params.append("specialties", spec)); + } if (opts.min_rating !== undefined && opts.min_rating > 0) params.append("min_rating", String(opts.min_rating)); + if (opts.max_price !== undefined && opts.max_price > 0) params.append("max_price", String(opts.max_price)); + if (opts.min_experience !== undefined && opts.min_experience > 0) params.append("min_experience", String(opts.min_experience)); if (opts.is_available !== undefined) params.append("is_available", String(opts.is_available)); return request(`/artisans/nearby?${params}`); }, From e3050690dedfed5715905c1c6fd25df384da3d94 Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 28 Apr 2026 03:14:46 +0100 Subject: [PATCH 2/5] style: run black formatting on artisan endpoints --- backend/app/api/v1/endpoints/artisan.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/app/api/v1/endpoints/artisan.py b/backend/app/api/v1/endpoints/artisan.py index ef2f177..a50bbc5 100644 --- a/backend/app/api/v1/endpoints/artisan.py +++ b/backend/app/api/v1/endpoints/artisan.py @@ -81,14 +81,16 @@ async def get_nearby_artisans( radius_km: float = Query( 25.0, ge=0, le=200, description="Search radius in kilometers" ), - specialties: list[str] - | None = Query(None, description="Filter by multiple skill keywords"), - min_rating: float - | None = Query(None, ge=0, le=5, description="Minimum average rating"), - max_price: float - | None = Query(None, ge=0, description="Maximum hourly rate"), - min_experience: int - | None = Query(None, ge=0, description="Minimum years of experience"), + specialties: list[str] | None = Query( + None, description="Filter by multiple skill keywords" + ), + min_rating: float | None = Query( + None, ge=0, le=5, description="Minimum average rating" + ), + max_price: float | None = Query(None, ge=0, description="Maximum hourly rate"), + min_experience: int | None = Query( + None, ge=0, description="Minimum years of experience" + ), available: bool | None = Query(None, description="Filter by current availability"), page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), From 55dbdb753dc181e7d47fe1ed0a9ead52b77b1303 Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 28 Apr 2026 03:47:39 +0100 Subject: [PATCH 3/5] fix: resolve black formatting limits and add missing filter params to list_artisans --- artisan_old.py | Bin 0 -> 37036 bytes backend/app/api/v1/endpoints/artisan.py | 20 +++++++++----------- backend/app/services/artisan.py | 8 ++++++++ 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 artisan_old.py diff --git a/artisan_old.py b/artisan_old.py new file mode 100644 index 0000000000000000000000000000000000000000..1440f3f1d23ce4925428c2da3a04f6280fbcf383 GIT binary patch literal 37036 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 R>s|m!^AxA%+dfJ5{{eozgUtW{ literal 0 HcmV?d00001 diff --git a/backend/app/api/v1/endpoints/artisan.py b/backend/app/api/v1/endpoints/artisan.py index a50bbc5..f2fc2ae 100644 --- a/backend/app/api/v1/endpoints/artisan.py +++ b/backend/app/api/v1/endpoints/artisan.py @@ -81,17 +81,11 @@ async def get_nearby_artisans( radius_km: float = Query( 25.0, ge=0, le=200, description="Search radius in kilometers" ), - specialties: list[str] | None = Query( - None, description="Filter by multiple skill keywords" - ), - min_rating: float | None = Query( - None, ge=0, le=5, description="Minimum average rating" - ), - max_price: float | None = Query(None, ge=0, description="Maximum hourly rate"), - min_experience: int | None = Query( - None, ge=0, description="Minimum years of experience" - ), - available: bool | None = Query(None, description="Filter by current availability"), + specialties: list[str] | None = Query(None, description="Filter by skills"), + min_rating: float | None = Query(None, ge=0, le=5, description="Min rating"), + max_price: float | None = Query(None, ge=0, description="Max hourly rate"), + min_experience: int | None = Query(None, ge=0, description="Min experience"), + available: bool | None = Query(None, description="Filter by availability"), page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), ): @@ -473,6 +467,8 @@ def list_artisans( limit: int = Query(20, ge=1, le=100), specialties: list[str] | None = Query(None), min_rating: float | None = Query(None, ge=0, le=5), + max_price: float | None = Query(None, ge=0), + min_experience: int | None = Query(None, ge=0), is_available: bool | None = Query(None), has_location: bool | None = Query(None), ): @@ -483,6 +479,8 @@ def list_artisans( limit=limit, specialties=specialties, min_rating=min_rating, + max_price=max_price, + min_experience=min_experience, is_available=is_available, has_location=has_location, ) diff --git a/backend/app/services/artisan.py b/backend/app/services/artisan.py index 260a73e..89a2b07 100644 --- a/backend/app/services/artisan.py +++ b/backend/app/services/artisan.py @@ -162,6 +162,8 @@ def list_artisans( limit: int = 100, specialties: list[str] | None = None, min_rating: float | None = None, + max_price: float | None = None, + min_experience: int | None = None, is_available: bool | None = None, has_location: bool | None = None, ) -> list[Artisan]: @@ -179,6 +181,12 @@ def list_artisans( if min_rating is not None: query = query.filter(Artisan.rating >= min_rating) + if max_price is not None: + query = query.filter(Artisan.hourly_rate <= max_price) + + if min_experience is not None: + query = query.filter(Artisan.experience_years >= min_experience) + if is_available is not None: query = query.filter(Artisan.is_available == is_available) From ceafa68e904f6091da3763026f242523be5c7bb7 Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 28 Apr 2026 03:48:17 +0100 Subject: [PATCH 4/5] chore: remove accidental temp file --- artisan_old.py | Bin 37036 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 artisan_old.py diff --git a/artisan_old.py b/artisan_old.py deleted file mode 100644 index 1440f3f1d23ce4925428c2da3a04f6280fbcf383..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37036 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 R>s|m!^AxA%+dfJ5{{eozgUtW{ From 1ede2540987ecc5ac183f546b61c63191024eaaa Mon Sep 17 00:00:00 2001 From: AugistineCreates Date: Tue, 28 Apr 2026 04:05:59 +0100 Subject: [PATCH 5/5] fix: resolve WalletContext memory leaks and instantiation issues --- frontend/context/WalletContext.tsx | 45 ++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/frontend/context/WalletContext.tsx b/frontend/context/WalletContext.tsx index b7b4341..d1b24eb 100644 --- a/frontend/context/WalletContext.tsx +++ b/frontend/context/WalletContext.tsx @@ -7,6 +7,7 @@ import { useCallback, useMemo, useEffect, + useRef, ReactNode, } from "react"; @@ -41,23 +42,43 @@ export function WalletProvider({ children }: { children: ReactNode }) { const [address, setAddress] = useState(null); const [kit, setKit] = useState(null); + const kitRef = useRef(null); + useEffect(() => { - import("@creit.tech/stellar-wallets-kit").then( - ({ - StellarWalletsKit: Kit, - WalletNetwork, - allowAllModules, - FREIGHTER_ID, - }) => { - setKit( - new Kit({ + let isMounted = true; + + if (!kitRef.current) { + import("@creit.tech/stellar-wallets-kit").then( + ({ + StellarWalletsKit: Kit, + WalletNetwork, + allowAllModules, + FREIGHTER_ID, + }) => { + if (!isMounted) return; + const newKit = new Kit({ network: WalletNetwork.TESTNET, selectedWalletId: FREIGHTER_ID, modules: allowAllModules(), - }) - ); + }); + kitRef.current = newKit as WalletKitInstance; + setKit(kitRef.current); + } + ); + } + + return () => { + isMounted = false; + // Ensure event listeners for the wallet kit are properly cleaned up on unmount + if (kitRef.current) { + const currentKit = kitRef.current as any; + if (typeof currentKit.removeEventListeners === "function") { + currentKit.removeEventListeners(); + } else if (typeof currentKit.disconnect === "function") { + currentKit.disconnect(); + } } - ); + }; }, []); const connect = useCallback(async () => {