diff --git a/backend/app/api/v1/endpoints/artisan.py b/backend/app/api/v1/endpoints/artisan.py
index b28a35a..f2fc2ae 100644
--- a/backend/app/api/v1/endpoints/artisan.py
+++ b/backend/app/api/v1/endpoints/artisan.py
@@ -81,11 +81,11 @@ 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)"),
- min_rating: float
- | None = Query(None, ge=0, le=5, description="Minimum average rating"),
- 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),
):
@@ -97,8 +97,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
)
@@ -465,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),
):
@@ -475,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/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..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)
@@ -231,6 +239,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 03d2d02..94765a7 100644
--- a/frontend/app/artisans/page.tsx
+++ b/frontend/app/artisans/page.tsx
@@ -5,173 +5,14 @@ import Link from "next/link";
import Navbar from "../../components/ui/Navbar";
import Footer from "../../components/ui/Footer";
import { Button } from "../../components/ui/button";
-import {
- Card,
- CardContent,
-} from "../../components/ui/card";
+import { Card, CardContent } from "../../components/ui/card";
import { api, type ArtisanItem } from "../../lib/api";
-import { Wrench, MapPin, Star, Sparkles, Filter, X, SlidersHorizontal } from "lucide-react";
+import { Wrench, MapPin, Star, Sparkles } from "lucide-react";
import Price from "../../components/ui/Price";
const DEFAULT_LAT = 51.5074;
const DEFAULT_LON = -0.1278;
-function SkeletonBlock({
- className,
-}: {
- className: string;
-}) {
- return
;
-}
-
-function ArtisanSkeletonCard() {
- return (
-
-
-
-
-
- );
-}
-
-function MapSkeleton() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 3 }).map((_, index) => (
-
-
-
-
- ))}
-
-
-
-
- );
-}
-
-function ArtisanMapPanel({
- artisans,
- loading,
- hasResults,
-}: {
- artisans: ArtisanItem[];
- loading: boolean;
- hasResults: boolean;
-}) {
- if (loading && !hasResults) {
- return ;
- }
-
- const mappedArtisans = artisans
- .filter((artisan) => artisan.latitude != null && artisan.longitude != null)
- .slice(0, 6);
-
- return (
-
-
-
-
-
-
-
Nearby map view
-
- {artisans.length > 0
- ? `${artisans.length} artisan${artisans.length === 1 ? "" : "s"} in this search`
- : "No mappable artisans yet"}
-
-
-
-
- {mappedArtisans.length > 0 ? (
- mappedArtisans.map((artisan, index) => {
- const x = 18 + (index % 3) * 24 + (index % 2) * 5;
- const y = 26 + Math.floor(index / 3) * 28 + (index % 2) * 8;
- return (
-
-
-
-
-
-
{artisan.business_name || "Artisan"}
-
-
- );
- })
- ) : (
-
-
-
Map coordinates unavailable
-
- Results still load below while location details catch up.
-
-
-
- )}
-
-
-
-
- {artisans.slice(0, 3).map((artisan) => (
-
-
- {artisan.business_name || specialtyLabel(artisan)}
-
-
- {artisan.location || "Location pending"}
-
-
- ))}
-
-
-
- {loading && (
-
-
-
- Refreshing results...
-
-
- )}
-
-
-
- );
-}
-
function specialtyLabel(artisan: ArtisanItem) {
const s = artisan.specialties;
if (Array.isArray(s)) return s[0] ?? "Artisan";
@@ -184,412 +25,256 @@ export default function ArtisansPage() {
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
-
+
const [lat, setLat] = useState(null);
const [lon, setLon] = useState(null);
- const [locationStatus, setLocationStatus] = useState<"pending" | "granted" | "denied">("pending");
const [page, setPage] = useState(1);
const pageSize = 12;
// Filters
- const [skill, setSkill] = useState("");
- const [minRating, setMinRating] = useState(0);
- const [isAvailable, setIsAvailable] = useState(false);
- const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
-
- // Debounced filters
- const [debouncedFilters, setDebouncedFilters] = useState({ skill, minRating, isAvailable });
+ const [specialties, setSpecialties] = useState([]);
+ const [minRating, setMinRating] = useState(0);
+ const [maxPrice, setMaxPrice] = useState("");
+ const [minExperience, setMinExperience] = useState("");
+ const [isAvailable, setIsAvailable] = useState(false);
+
+ const [debouncedFilters, setDebouncedFilters] = useState({
+ specialties,
+ minRating,
+ maxPrice,
+ minExperience,
+ isAvailable,
+ });
useEffect(() => {
const handler = setTimeout(() => {
- setDebouncedFilters({ skill, minRating, isAvailable });
- setPage(1); // reset to page 1 on filter
+ setDebouncedFilters({
+ specialties,
+ minRating,
+ maxPrice,
+ minExperience,
+ isAvailable,
+ });
+ setPage(1);
}, 500);
return () => clearTimeout(handler);
- }, [skill, minRating, isAvailable]);
+ }, [specialties, minRating, maxPrice, minExperience, isAvailable]);
- const requestLocation = () => {
- if (typeof window === "undefined" || !navigator.geolocation) {
- setLocationStatus("denied");
- setLat(DEFAULT_LAT);
- setLon(DEFAULT_LON);
- return;
+ useEffect(() => {
+ if (!navigator.geolocation) {
+ setLat(DEFAULT_LAT);
+ setLon(DEFAULT_LON);
+ return;
}
- setLocationStatus("pending");
+
navigator.geolocation.getCurrentPosition(
(pos) => {
setLat(pos.coords.latitude);
setLon(pos.coords.longitude);
- setLocationStatus("granted");
},
() => {
setLat(DEFAULT_LAT);
setLon(DEFAULT_LON);
- setLocationStatus("denied");
}
);
- };
-
- useEffect(() => {
- requestLocation();
}, []);
useEffect(() => {
if (lat === null || lon === null) return;
- let isMounted = true;
+
setLoading(true);
setError("");
api.artisans
- .nearby(lat, lon, {
- page,
+ .nearby(lat, lon, {
+ page,
page_size: pageSize,
- skill: debouncedFilters.skill || undefined,
+ specialties: debouncedFilters.specialties.length
+ ? debouncedFilters.specialties
+ : undefined,
min_rating: debouncedFilters.minRating || undefined,
- is_available: debouncedFilters.isAvailable ? true : undefined,
+ max_price:
+ debouncedFilters.maxPrice !== ""
+ ? Number(debouncedFilters.maxPrice)
+ : undefined,
+ min_experience:
+ debouncedFilters.minExperience !== ""
+ ? Number(debouncedFilters.minExperience)
+ : undefined,
+ is_available: debouncedFilters.isAvailable || undefined,
})
.then((res) => {
- if (!isMounted) return;
setArtisans(res.items);
setTotal(res.total);
})
.catch((err) => {
- if (!isMounted) return;
setError(err instanceof Error ? err.message : "Failed to load artisans");
})
- .finally(() => {
- if (isMounted) setLoading(false);
- });
-
- return () => {
- isMounted = false;
- };
+ .finally(() => setLoading(false));
}, [lat, lon, page, debouncedFilters]);
const clearFilters = () => {
- setSkill("");
+ setSpecialties([]);
setMinRating(0);
+ setMaxPrice("");
+ setMinExperience("");
setIsAvailable(false);
};
- const hasLoadedResults = artisans.length > 0 || total > 0;
- const showInitialSkeleton = loading && !hasLoadedResults && !error;
-
return (
-
+
-
-
- Find an Artisan
-
-
- Artisans near you ready to help.
-
-
- {locationStatus === "denied" && (
-
-
Location access denied. Displaying results for default location.
-
Retry Location
-
- )}
- {/* Desktop Filter Bar */}
-
-
- Specialty
- setSkill(e.target.value)}
- className="w-full rounded-md border border-gray-300 px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none bg-white transition-all hover:border-gray-400"
- >
- Any Specialty
- Plumber
- Electrician
- Carpenter
- Painter
- Mechanic
-
-
-
-
- Min Rating ({minRating} Stars)
-
- setMinRating(Number(e.target.value))}
- className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
- />
-
-
- setIsAvailable(e.target.checked)}
- className="w-5 h-5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 cursor-pointer"
- />
-
- Available Now
-
-
-
- Reset
-
-
-
- {/* Mobile Filter Button */}
-
-
setIsFilterDrawerOpen(true)}
- variant="outline"
- className="flex items-center gap-2 bg-white border-gray-200 text-gray-700 shadow-sm"
- >
-
- Filters {(skill || minRating > 0 || isAvailable) && }
-
-
- {total} results
-
-
-
- {/* Mobile Filter Drawer */}
- {isFilterDrawerOpen && (
-
-
setIsFilterDrawerOpen(false)}
- />
-
-
-
Filters
- setIsFilterDrawerOpen(false)}
- className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
- >
-
-
-
-
-
-
- Specialty
- setSkill(e.target.value)}
- className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 focus:border-blue-600 focus:outline-none bg-gray-50"
- >
- Any Specialty
- Plumber
- Electrician
- Carpenter
- Painter
- Mechanic
-
-
-
-
-
- Min Rating ({minRating} Stars)
-
-
setMinRating(Number(e.target.value))}
- className="w-full h-2 bg-gray-100 rounded-lg appearance-none cursor-pointer accent-blue-600"
- />
-
- 0
- 1
- 2
- 3
- 4
- 5
-
-
-
-
-
- Available Now
-
-
+
Find an Artisan
+
Artisans near you ready to help.
+
+
+
+ {/* Sidebar */}
+
+ Filters
+
+ {/* Specialties */}
+
+
Specialties
+ {["Plumber", "Electrician", "Carpenter", "Painter", "Mechanic"].map((s) => (
+
+ setIsAvailable(e.target.checked)}
- className="w-6 h-6 text-blue-600 bg-white border-gray-300 rounded-md focus:ring-blue-500 cursor-pointer"
- />
-
-
+ checked={specialties.includes(s)}
+ onChange={(e) =>
+ e.target.checked
+ ? setSpecialties([...specialties, s])
+ : setSpecialties(specialties.filter((x) => x !== s))
+ }
+ />{" "}
+ {s}
+
+ ))}
+
-
- {
- clearFilters();
- setIsFilterDrawerOpen(false);
- }}
- >
- Reset
-
- setIsFilterDrawerOpen(false)}
- >
- Apply Filters
-
-
+ {/* Rating */}
+
+
Min Rating: {minRating}
+
setMinRating(Number(e.target.value))}
+ />
-
- )}
- {error && (
-
{error}
- )}
+ {/* Price */}
+
+
Max Price
+
+ setMaxPrice(e.target.value ? Number(e.target.value) : "")
+ }
+ className="w-full border p-2"
+ />
+
-
-
-
-
-
- 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"}
-
+ {/* Experience */}
+
+
Min Experience
+
+ setMinExperience(e.target.value ? Number(e.target.value) : "")
+ }
+ className="w-full border p-2"
+ />
-
-
-
- {showInitialSkeleton ? (
-
- {Array.from({ length: 6 }).map((_, i) => (
-
- ))}
-
- ) : artisans.length === 0 ? (
-
-
-
No artisans found
-
Try adjusting your filters to find what you're looking for.
-
Clear Filters
-
- ) : (
- <>
-
-
- {artisans.map((a) => (
-
-
-
-
-
-
-
-
-
-
+ {/* Availability */}
+
+ setIsAvailable(e.target.checked)}
+ />{" "}
+ Available Now
+
+
+
+ Clear Filters
+
+
+
+ {/* Main Content */}
+
+ {error &&
{error}
}
+
+ {loading ? (
+
Loading...
+ ) : artisans.length === 0 ? (
+
+
+
No artisans found
+
Clear Filters
+
+ ) : (
+ <>
+
+ {artisans.map((a) => (
+
+
+
+
{a.business_name || specialtyLabel(a)}
-
-
- {a.location || "Location not set"}
+
+
+ {a.location}
-
- {a.rating != null && (
-
-
- {Number(a.rating).toFixed(1)}
-
- )}
- {a.hourly_rate != null && (
-
- /hr
-
- )}
-
-
-
-
- {a.is_available && (
-
- Available now
-
+
+ {a.rating && (
+
+ {a.rating}
+
)}
- {a.distance_km != null && (
-
- {Number(a.distance_km).toFixed(1)} km away
-
+
+ {a.hourly_rate && (
+
+ /hr
+
)}
-
-
-
-
-
- ))}
-
- {loading && (
-
- )}
-
- {total > pageSize && (
-
-
setPage((p) => p - 1)}
- className="w-24"
- >
- Previous
-
-
- Page {page} of {Math.ceil(total / pageSize)}
+
+
+
+ ))}
-
= Math.ceil(total / pageSize)}
- onClick={() => setPage((p) => p + 1)}
- className="w-24"
- >
- Next
-
-
+
+ {/* Pagination */}
+ {total > pageSize && (
+
+ setPage(page - 1)}>
+ Prev
+
+
+ {page} / {Math.ceil(total / pageSize)}
+
+ = Math.ceil(total / pageSize)}
+ onClick={() => setPage(page + 1)}
+ >
+ Next
+
+
+ )}
+ >
)}
- >
- )}
+
+
+
);
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 () => {
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts
index c5b9608..51aa50f 100644
--- a/frontend/lib/api.ts
+++ b/frontend/lib/api.ts
@@ -13,39 +13,52 @@ async function request(
options: RequestInit & { token?: string } = {},
): Promise {
const { token, ...init } = options;
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
+
const headers: HeadersInit = {
...(init.headers as Record),
};
+
if (!(init.body instanceof FormData)) {
headers["Content-Type"] = "application/json";
}
+
if (token) {
(headers as Record)["Authorization"] = `Bearer ${token}`;
}
+
const res = await fetch(url, { ...init, headers });
const text = await res.text();
+
if (!res.ok) {
let message = res.statusText;
+
try {
const json = JSON.parse(text);
- message =
- json.detail ??
- (typeof json.detail === "string" ? json.detail : message);
- if (Array.isArray(json.detail))
+
+ if (typeof json.detail === "string") {
+ message = json.detail;
+ } else if (Array.isArray(json.detail)) {
message =
json.detail.map((d: { msg?: string }) => d.msg ?? "").join("; ") ||
message;
+ }
} catch {
if (text) message = text;
}
+
throw new Error(message);
}
+
if (!text) return undefined as T;
+
return JSON.parse(text) as T;
}
-// --- Types (aligned with backend schemas) ---
+/* =========================
+ TYPES
+========================= */
export interface UserOut {
id: number;
@@ -155,7 +168,9 @@ interface PaginatedArtisansResponse {
page_size: number;
}
-// --- API object ---
+/* =========================
+ API
+========================= */
export const api = {
auth: {
@@ -164,6 +179,7 @@ export const api = {
method: "POST",
body: JSON.stringify(body),
}),
+
register: (body: {
email: string;
password: string;
@@ -176,18 +192,22 @@ export const api = {
body: JSON.stringify(body),
}),
},
+
users: {
me: (token: string) =>
request("/users/me", { method: "GET", token }),
+
updateMe: (body: Partial, token: string) =>
request("/users/me", {
method: "PUT",
body: JSON.stringify(body),
token,
}),
+
uploadAvatar: (file: File, token: string) => {
const formData = new FormData();
formData.append("file", file);
+
return request("/users/me/avatar", {
method: "POST",
body: formData,
@@ -195,17 +215,21 @@ export const api = {
});
},
},
+
artisans: {
me: (token: string) =>
request("/artisans/me", { method: "GET", token }),
+
nearby: (
lat: number,
lon: number,
opts: {
page?: number;
page_size?: number;
- skill?: string;
+ specialties?: string[];
min_rating?: number;
+ max_price?: number;
+ min_experience?: number;
is_available?: boolean;
} = {},
) => {
@@ -215,15 +239,35 @@ 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.min_rating !== undefined && opts.min_rating > 0)
+
+ if (opts.specialties && opts.specialties.length > 0) {
+ opts.specialties.forEach((s) => params.append("specialties", s));
+ }
+
+ if (opts.min_rating && opts.min_rating > 0) {
params.append("min_rating", String(opts.min_rating));
- if (opts.is_available !== undefined)
+ }
+
+ if (opts.max_price && opts.max_price > 0) {
+ params.append("max_price", String(opts.max_price));
+ }
+
+ if (opts.min_experience && 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}`);
+ }
+
+ return request(
+ `/artisans/nearby?${params.toString()}`,
+ );
},
+
getProfile: (artisanId: number) =>
request(`/artisans/${artisanId}/profile`),
+
updateProfile: (body: ArtisanProfileUpdate, token: string) =>
request("/artisans/profile", {
method: "PUT",
@@ -231,12 +275,14 @@ export const api = {
token,
}),
},
+
bookings: {
myBookings: (token: string) =>
request("/bookings/my-bookings", {
method: "GET",
token,
}),
+
create: (body: BookingCreate, token: string) =>
request("/bookings/create", {
method: "POST",
@@ -244,8 +290,9 @@ export const api = {
token,
}),
},
+
notifications: {
- get: (token: string, skip: number = 0, limit: number = 50) =>
+ get: (token: string, skip = 0, limit = 50) =>
request(
`/notifications/?skip=${skip}&limit=${limit}`,
{
@@ -253,25 +300,35 @@ export const api = {
token,
},
),
+
getUnreadCount: (token: string) =>
request<{ unread_count: number }>("/notifications/unread-count", {
method: "GET",
token,
}),
+
markAsRead: (token: string, notificationId: string) =>
- request(`/notifications/${notificationId}/read`, {
- method: "PUT",
- token,
- }),
+ request(
+ `/notifications/${notificationId}/read`,
+ {
+ method: "PUT",
+ token,
+ },
+ ),
+
markAllAsRead: (token: string) =>
request<{ message: string }>("/notifications/mark-all-read", {
method: "PUT",
token,
}),
+
delete: (token: string, notificationId: string) =>
- request<{ message: string }>(`/notifications/${notificationId}`, {
- method: "DELETE",
- token,
- }),
+ request<{ message: string }>(
+ `/notifications/${notificationId}`,
+ {
+ method: "DELETE",
+ token,
+ },
+ ),
},
-};
+};
\ No newline at end of file