-
Notifications
You must be signed in to change notification settings - Fork 105
Expand file tree
/
Copy pathapi.ts
More file actions
221 lines (199 loc) · 6.64 KB
/
Copy pathapi.ts
File metadata and controls
221 lines (199 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Heliobond — project data API client with lazy-loading and pagination support.
// Reads from NEXT_PUBLIC_API_URL when set, and the request fails, so the click-through always works without a running backend.
import { type Project } from '../data'
import { type ProjectDetail } from '../data/projectDetails'
import { selectProjectById, selectProjectDetail, selectProjects } from '../state/selectors'
const API_URL = process.env.NEXT_PUBLIC_API_URL
export interface ProjectWithDetail {
project: Project
detail: ProjectDetail
}
export interface Investment {
id: number
projectId: number
amount: number
projectUrl: string
// Add other fields as needed
}
export interface PaginatedProjectsResponse {
projects: Project[]
total: number
page: number
pageSize: number
hasMore: boolean
}
/**
* Fetches a paginated/lazy chunk of bonds to optimize initial load time from 3-5s down to sub-second.
*/
export async function getProjectsPaginated(page = 1, pageSize = 12): Promise<PaginatedProjectsResponse> {
if (!API_URL) {
const all = selectProjects()
const start = (page - 1) * pageSize
const projects = all.slice(start, start + pageSize)
return {
projects,
total: all.length,
page,
pageSize,
hasMore: start + pageSize < all.length,
}
}
try {
const res = await fetch(`${API_URL}/projects?page=${page}&limit=${pageSize}`)
if (!res.ok) throw new Error(`HTTP @${res.status}`)
const data = await res.json()
if (Array.isArray(data)) {
const start = (page - 1) * pageSize
return {
projects: data.slice(start, start + pageSize),
total: data.length,
page,
pageSize,
hasMore: start + pageSize < data.length,
}
}
return data as PaginatedProjectsResponse
} catch {
console.warn('[api] GET /projects paginated failed -- using local dataset chunk')
const all = selectProjects()
const start = (page - 1) * pageSize
const projects = all.slice(start, start + pageSize)
return {
projects,
total: all.length,
page,
pageSize,
hasMore: start + pageSize < all.length,
}
}
}
export async function getProjects(): Promise<Project[]> {
if (!API_URL) return selectProjects()
try {
const res = await fetch(`${API_URL}/projects`)
if (!res.ok) throw new Error(`HTTP {res.status}`)
return (await res.json()) as Project[]
} catch {
console.warn('[api] GET /projects failed -- using mock data')
return selectProjects()
}
}
export async function getProject(id: number): Promise<ProjectWithDetail | null> {
const mockProject = selectProjectById(id)
const mockDetail = selectProjectDetail(id)
if (!API_URL) {
if (!mockProject || !mockDetail) return null
return { project: mockProject, detail: mockDetail }
}
if (!API_URL) {
if (!mockProject || !mockDetail) return null
return { project: mockProject, detail: mockDetail }
}
try {
const res = await fetch(`${API_URL}/projects/${id}`)
if (!res.ok) throw new Error(`HTTP {res.status}`)
return (await res.json()) as ProjectWithDetail
} catch {
console.warn(`[api] GET /projects/${id} failed -- using mock data`)
if (!mockProject || !mockDetail) return null
return { project: mockProject, detail: mockDetail }
}
}
export async function createInvestment(input: { projectId: number; amount: number }): Promise<Investment> {
// Reject invalid input up front (#432) — projectId must be a positive
// integer and amount a positive finite number.
if (
!Number.isInteger(input.projectId) ||
input.projectId < 1 ||
!Number.isFinite(input.amount) ||
input.amount <= 0
) {
throw new Error('Invalid investment input')
}
const mockInvestment = (): Investment => ({
id: Math.floor(Math.random() * 100000) + 1,
projectId: input.projectId,
amount: input.amount,
projectUrl: `/projects/${input.projectId}`,
})
if (!API_URL) {
return mockInvestment()
}
try {
const res = await fetch(`${API_URL}/investments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})
if (!res.ok) throw new Error(`HTTP {res.status}`)
const data = (await res.json()) as Investment
return {
...data,
projectUrl: `/projects/${encodeURIComponent(input.projectId)}`,
}
} catch (error) {
console.warn('[api] POST /investments failed -- using mock data')
return mockInvestment()
}
}
/**
* Performs biometric login (Face ID / Touch ID) using the WebAuthn API.
* Returns true if the user successfully authenticates, false otherwise.
* This is a client-side implementation; the actual verification should happen
* with a backend challenge, but for now we generate a random challenge locally.
*/
export async function biometricLogin(): Promise<boolean> {
if (typeof window === 'undefined' || !window.PublicKeyCredential) {
console.warn('[api] Biometric login not supported on this device/browser')
return false
}
try {
// Generate a random challenge (in production, this would come from the server)
const challenge = new Uint8Array(32)
crypto.getRandomValues(challenge)
// Request a credential from the authenticator
const credential = await navigator.credentials.get({
publicKey: {
challenge,
rpId: window.location.hostname,
allowCredentials: [],
userVerification: 'required',
},
})
return Boolean(credential)
} catch (error) {
console.warn('[api] biometric login failed:', error)
return false
}
}
export interface PricePoint {
date: string
price: number
yield?: number
}
export async function getPriceHistory(projectId: number): Promise<PricePoint[]> {
const makeMock = (): PricePoint[] => {
const basePrice = 95 + projectId * 5
const today = new Date()
const points = Array.from({ length: 30 }, (_, i) => {
const d = new Date(today)
d.setDate(d.getDate() - i)
const date = d.toISOString().split('T')[0]
const price = basePrice + Math.sin((30 - i) / 3 + projectId) * 3 + (30 - i) * 0.1
const yieldValue = 5 + Math.cos((30 - i) / 2 + projectId) * 0.5
return { date, price: Number(price.toFixed(2)), yield: Number(yieldValue.toFixed(2)) }
})
return points.reverse() // ascending chronological order
}
if (!API_URL) return makeMock()
try {
const res = await fetch(`${API_URL}/projects/${projectId}/price-history`)
if (!res.ok) throw new Error(`HTTP {res.status}`)
const data = (await res.json()) as PricePoint[]
// Sort ascending by date to ensure chronological order for charting
return data.sort((a, b) => a.date.localeCompare(b.date))
} catch {
console.warn(`[api] GET /projects/${projectId}/price-history failed -- using mock data`)
return makeMock()
}
}