source: petify-frontend/src/api/listings.ts@ 92e7c7a

Last change on this file since 92e7c7a was 92e7c7a, checked in by veronika-ils <ilioskaveronika@…>, 11 hours ago

Petify fullstack project

  • Property mode set to 100644
File size: 6.3 KB
Line 
1import type { Listing } from '../types/listing'
2
3function getBaseUrl(): string {
4 const base = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? ''
5 return base.replace(/\/$/, '')
6}
7
8function joinUrl(base: string, path: string): string {
9 if (!base) return path
10 return `${base}${path.startsWith('/') ? '' : '/'}${path}`
11}
12
13type UnknownRecord = Record<string, unknown>
14
15function toNumber(value: unknown): number | undefined {
16 if (value === null || value === undefined || value === '') return undefined
17 if (typeof value === 'number' && Number.isFinite(value)) return value
18 const n = Number(value)
19 return Number.isFinite(n) ? n : undefined
20}
21
22function toString(value: unknown): string | undefined {
23 if (value === null || value === undefined) return undefined
24 const s = String(value)
25 return s ? s : undefined
26}
27
28function mapStatus(status: string | undefined): string | undefined {
29 if (!status) return undefined
30 switch (status) {
31 case 'DRAFT':
32 return 'Draft'
33 case 'ACTIVE':
34 return 'Active'
35 case 'SOLD':
36 return 'Sold'
37 case 'ARCHIVED':
38 return 'Archived'
39 default:
40 return status
41 }
42}
43
44function titleFromDescription(description: string | undefined): string | undefined {
45 if (!description) return undefined
46 const firstLine = description.split(/\r?\n/)[0]?.trim()
47 if (!firstLine) return undefined
48 // Use a short, readable title.
49 const max = 52
50 return firstLine.length > max ? `${firstLine.slice(0, max - 1).trimEnd()}…` : firstLine
51}
52
53function normalizeListingRow(row: unknown): Listing | null {
54 if (!row || typeof row !== 'object') return null
55 const r = row as UnknownRecord
56
57 const listingId =
58 toNumber(r.listing_id) ?? toNumber(r.listingId) ?? toNumber(r.id)
59
60 const ownerId = toNumber(r.owner_id) ?? toNumber(r.ownerId)
61 const animalId = toNumber(r.animal_id) ?? toNumber(r.animalId)
62
63 const rawStatus = toString(r.status)
64 const price = toNumber(r.price)
65 const description = toString(r.description)
66 const createdAt = toString(r.created_at) ?? toString(r.createdAt)
67 const animalName = toString(r.animal_name) ?? toString(r.animalName)
68 const ownerName = toString(r.owner_name) ?? toString(r.ownerName)
69 const ownerEmail = toString(r.owner_email) ?? toString(r.ownerEmail)
70 const species = toString(r.species)
71 const breed = toString(r.breed)
72 const city = toString(r.located_name) ?? toString(r.locatedName) ?? toString(r.city)
73 const imageUrl = toString(r.photo_url) ?? toString(r.photoUrl) ?? toString(r.imageUrl)
74
75 // If backend doesn't provide a display title, make a reasonable one.
76 const title =
77 toString(r.title) ??
78 animalName ??
79 toString(r.name) ??
80 titleFromDescription(description) ??
81 (listingId !== undefined ? `Listing #${listingId}` : 'Listing')
82
83 // `id` is required by the UI.
84 const id = listingId !== undefined ? String(listingId) : toString(r.id) ?? ''
85 if (!id) return null
86
87 return {
88 id,
89 title,
90 listingId,
91 ownerId,
92 animalId,
93 status: mapStatus(rawStatus),
94 price,
95 description,
96 createdAt,
97 animalName,
98 ownerName,
99 ownerEmail,
100 species,
101 petType: species,
102 breed,
103 city,
104 imageUrl,
105 }
106}
107
108export async function fetchListings(options?: { signal?: AbortSignal }): Promise<Listing[]> {
109 const url = joinUrl(getBaseUrl(), '/api/public/listings')
110 const res = await fetch(url, {
111 method: 'GET',
112 headers: { Accept: 'application/json' },
113 signal: options?.signal,
114 })
115
116 if (!res.ok) {
117 throw new Error(`Failed to load listings (${res.status})`)
118 }
119
120 const data = (await res.json()) as unknown
121 if (!Array.isArray(data)) return []
122
123 return data.map(normalizeListingRow).filter((x): x is Listing => x !== null)
124}
125
126export async function fetchListingById(listingId: string | number, options?: { signal?: AbortSignal }): Promise<Listing> {
127 const url = joinUrl(getBaseUrl(), `/api/listings/${listingId}`)
128 const res = await fetch(url, {
129 method: 'GET',
130 headers: { Accept: 'application/json' },
131 signal: options?.signal,
132 })
133
134 if (!res.ok) {
135 throw new Error(`Failed to load listing (${res.status})`)
136 }
137
138 const data = (await res.json()) as unknown
139 const listing = normalizeListingRow(data)
140 if (!listing) throw new Error('Invalid listing data')
141 return listing
142}
143
144export async function fetchUserName(userId: number, options?: { signal?: AbortSignal }): Promise<string | null> {
145 try {
146 const url = joinUrl(getBaseUrl(), `/api/users/${userId}`)
147 const res = await fetch(url, {
148 method: 'GET',
149 headers: { Accept: 'application/json' },
150 signal: options?.signal,
151 })
152
153 if (!res.ok) return null
154
155 const data = (await res.json()) as unknown
156 if (typeof data === 'object' && data !== null) {
157 const user = data as Record<string, unknown>
158 const firstName = user.firstName ? String(user.firstName) : ''
159 const lastName = user.lastName ? String(user.lastName) : ''
160 if (firstName || lastName) {
161 return `${firstName} ${lastName}`.trim()
162 }
163 if (user.username) {
164 return String(user.username)
165 }
166 }
167 return null
168 } catch {
169 return null
170 }
171}
172
173export async function fetchPetName(petId: number, options?: { signal?: AbortSignal }): Promise<string | null> {
174 try {
175 const url = joinUrl(getBaseUrl(), `/api/pets/${petId}`)
176 const res = await fetch(url, {
177 method: 'GET',
178 headers: { Accept: 'application/json' },
179 signal: options?.signal,
180 })
181
182 if (!res.ok) return null
183
184 const data = (await res.json()) as unknown
185 if (typeof data === 'object' && data !== null) {
186 const pet = data as Record<string, unknown>
187 if (pet.name) {
188 return String(pet.name)
189 }
190 }
191 return null
192 } catch {
193 return null
194 }
195}
196
197export async function fetchRecommendedListings(userId: number, options?: { signal?: AbortSignal }): Promise<Listing[]> {
198 const url = joinUrl(getBaseUrl(), '/api/listings/recommendations')
199 const res = await fetch(url, {
200 method: 'GET',
201 headers: {
202 Accept: 'application/json',
203 'X-User-Id': String(userId)
204 },
205 signal: options?.signal,
206 })
207
208 if (!res.ok) {
209 throw new Error(`Failed to fetch recommended listings: ${res.statusText}`)
210 }
211
212 const data = (await res.json()) as unknown
213 if (!Array.isArray(data)) return []
214 return data.map(normalizeListingRow).filter((item) => item !== null) as Listing[]
215}
Note: See TracBrowser for help on using the repository browser.