Files
Ping/frontend/src/api/client.ts
T
Admin 95a2d46408 Add Ping frontend
React 19 + TypeScript client built with Vite. Zustand for local state,
react-oidc-context for the login flow, a SignalR connection for chat and
presence events, and LiveKit's React components for voice/video calls.
2026-09-21 19:33:02 +02:00

115 lines
4.4 KiB
TypeScript

import type {
AttachmentKind,
AttachmentUploadResponse,
CallTokenResponse,
ChannelResponse,
ChannelType,
FriendshipResponse,
MessageResponse,
ServerResponse,
UserResponse,
} from '../types/api'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
export class ApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.status = status
}
}
export interface MessageAttachmentInput {
key: string
fileName: string
contentType: string
kind: AttachmentKind
}
export function createApiClient(getAccessToken: () => string | undefined) {
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const token = getAccessToken()
const response = await fetch(`${API_BASE_URL}${path}`, {
...init,
headers: {
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...init?.headers,
},
})
if (!response.ok) {
const body = await response.text().catch(() => '')
throw new ApiError(response.status, body || response.statusText)
}
if (response.status === 204) {
return undefined as T
}
return (await response.json()) as T
}
return {
getCurrentUser: () => request<UserResponse>('/users/me'),
getUserById: (id: string) => request<UserResponse>(`/users/${id}`),
searchUsers: (query: string) => request<UserResponse[]>(`/users/search?query=${encodeURIComponent(query)}`),
getFriends: () => request<FriendshipResponse[]>('/friends'),
getPendingFriendRequests: () => request<FriendshipResponse[]>('/friends/requests'),
sendFriendRequest: (addresseeId: string) =>
request<FriendshipResponse>('/friends/requests', { method: 'POST', body: JSON.stringify({ addresseeId }) }),
acceptFriendRequest: (id: string) => request<FriendshipResponse>(`/friends/requests/${id}/accept`, { method: 'POST' }),
rejectFriendRequest: (id: string) => request<void>(`/friends/requests/${id}/reject`, { method: 'POST' }),
removeFriend: (id: string) => request<void>(`/friends/${id}`, { method: 'DELETE' }),
blockUser: (userId: string) => request<void>(`/friends/block/${userId}`, { method: 'POST' }),
getServers: () => request<ServerResponse[]>('/servers'),
createServer: (name: string) => request<ServerResponse>('/servers', { method: 'POST', body: JSON.stringify({ name }) }),
getServerChannels: (serverId: string) => request<ChannelResponse[]>(`/servers/${serverId}/channels`),
createChannel: (serverId: string, name: string, type: ChannelType) =>
request<ChannelResponse>(`/servers/${serverId}/channels`, { method: 'POST', body: JSON.stringify({ name, type }) }),
getOrCreateDirectMessageChannel: (otherUserId: string) =>
request<ChannelResponse>(`/channels/dm/${otherUserId}`, { method: 'POST' }),
getChannelMessages: (channelId: string, before?: string, take = 50) =>
request<MessageResponse[]>(
`/channels/${channelId}/messages?take=${take}${before ? `&before=${before}` : ''}`,
),
sendMessage: (channelId: string, content: string, attachments: MessageAttachmentInput[] = [], replyToMessageId?: string) =>
request<MessageResponse>(`/channels/${channelId}/messages`, {
method: 'POST',
body: JSON.stringify({ content, replyToMessageId, attachments }),
}),
editMessage: (id: string, content: string) =>
request<MessageResponse>(`/messages/${id}`, { method: 'PATCH', body: JSON.stringify({ content }) }),
deleteMessage: (id: string) => request<void>(`/messages/${id}`, { method: 'DELETE' }),
requestAttachmentUploadUrl: (fileName: string, contentType: string) =>
request<AttachmentUploadResponse>('/attachments/upload-url', {
method: 'POST',
body: JSON.stringify({ fileName, contentType }),
}),
requestCallToken: (channelId: string) =>
request<CallTokenResponse>(`/channels/${channelId}/call-token`, { method: 'POST' }),
}
}
export async function uploadFileToPresignedUrl(uploadUrl: string, file: File | Blob, contentType: string): Promise<void> {
const response = await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: file,
})
if (!response.ok) {
throw new ApiError(response.status, `Failed to upload file: ${response.statusText}`)
}
}
export type ApiClient = ReturnType<typeof createApiClient>