275 lines
7.1 KiB
TypeScript
275 lines
7.1 KiB
TypeScript
import type {
|
|
CalendarEvent,
|
|
EventResponse,
|
|
EventStatus,
|
|
EventType,
|
|
EventVisibility,
|
|
Room,
|
|
RoomMember,
|
|
RoomPermissions,
|
|
RoomRole,
|
|
SignupStatus,
|
|
User,
|
|
} from "./types"
|
|
|
|
export const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, "") || "https://ben.de-roo.org/api"
|
|
|
|
const TOKEN_KEY = "calendar_token"
|
|
|
|
export function getToken(): string | null {
|
|
if (typeof window === "undefined") return null
|
|
return window.localStorage.getItem(TOKEN_KEY)
|
|
}
|
|
|
|
export function setToken(token: string) {
|
|
window.localStorage.setItem(TOKEN_KEY, token)
|
|
}
|
|
|
|
export function clearToken() {
|
|
window.localStorage.removeItem(TOKEN_KEY)
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
status: number
|
|
constructor(message: string, status: number) {
|
|
super(message)
|
|
this.status = status
|
|
this.name = "ApiError"
|
|
}
|
|
}
|
|
|
|
interface RequestOptions {
|
|
method?: string
|
|
body?: unknown
|
|
auth?: boolean
|
|
query?: Record<string, string | number | undefined>
|
|
}
|
|
|
|
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const { method = "GET", body, auth = true, query } = options
|
|
|
|
let url = `${API_BASE_URL}${path}`
|
|
if (query) {
|
|
const params = new URLSearchParams()
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
params.append(key, String(value))
|
|
}
|
|
}
|
|
const qs = params.toString()
|
|
if (qs) url += `?${qs}`
|
|
}
|
|
|
|
const headers: Record<string, string> = {}
|
|
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
|
|
if (auth) {
|
|
const token = getToken()
|
|
if (token) headers["Authorization"] = `Bearer ${token}`
|
|
}
|
|
|
|
let res: Response
|
|
try {
|
|
res = await fetch(url, {
|
|
method,
|
|
headers,
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
})
|
|
} catch (err) {
|
|
throw new ApiError(
|
|
"Could not reach the server. Check your connection and the API URL.",
|
|
0,
|
|
)
|
|
}
|
|
|
|
if (res.status === 401) {
|
|
clearToken()
|
|
throw new ApiError("Your session has expired. Please sign in again.", 401)
|
|
}
|
|
|
|
let data: any = null
|
|
const text = await res.text()
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text)
|
|
} catch {
|
|
data = text
|
|
}
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const detail =
|
|
(data && typeof data === "object" && (data.detail || data.message)) ||
|
|
(typeof data === "string" ? data : null) ||
|
|
`Request failed (${res.status})`
|
|
throw new ApiError(
|
|
Array.isArray(detail) ? detail.map((d: any) => d.msg).join(", ") : String(detail),
|
|
res.status,
|
|
)
|
|
}
|
|
|
|
return data as T
|
|
}
|
|
|
|
// ---------- Auth ----------
|
|
|
|
export async function login(username: string, password: string) {
|
|
const res = await request<{
|
|
status: string
|
|
access_token?: string
|
|
token_type?: string
|
|
}>("/login", { method: "POST", body: { username, password }, auth: false })
|
|
|
|
if (res.status !== "success" || !res.access_token) {
|
|
throw new ApiError("Invalid username or password.", 401)
|
|
}
|
|
setToken(res.access_token)
|
|
return res.access_token
|
|
}
|
|
|
|
export async function register(username: string, password: string, email: string) {
|
|
return request<{ status: string; user_id: number; email_verification: string }>(
|
|
"/create-user",
|
|
{ method: "POST", body: { username, password, email }, auth: false },
|
|
)
|
|
}
|
|
|
|
export async function getMe() {
|
|
return request<User>("/me")
|
|
}
|
|
|
|
// ---------- Rooms ----------
|
|
|
|
export async function getRooms() {
|
|
const res = await request<{ rooms: Room[] }>("/rooms")
|
|
return res.rooms
|
|
}
|
|
|
|
export async function createRoom(room_name: string, invite_code: string) {
|
|
return request<{ status: string; room_id: number; member_id: number }>("/create-room", {
|
|
method: "POST",
|
|
body: { room_name, invite_code },
|
|
})
|
|
}
|
|
|
|
export async function searchRoom(invite_code: string) {
|
|
return request<{ status: string; room_name?: string; reason?: string }>("/search-room", {
|
|
method: "POST",
|
|
body: { invite_code },
|
|
})
|
|
}
|
|
|
|
export async function joinRoom(invite_code: string) {
|
|
return request<{ status: string; room_id?: number; reason?: string }>("/join-room", {
|
|
method: "POST",
|
|
body: { invite_code },
|
|
})
|
|
}
|
|
|
|
export async function getRoomMembers(roomId: number) {
|
|
const res = await request<{ members: RoomMember[] }>(`/rooms/${roomId}/members`)
|
|
return res.members
|
|
}
|
|
|
|
export async function roomWhoami(roomId: number) {
|
|
return request<RoomPermissions>(`/rooms/${roomId}/whoami`)
|
|
}
|
|
|
|
export async function renameRoom(roomId: number, new_name: string) {
|
|
return request(`/rooms/${roomId}`, { method: "PATCH", body: { new_name } })
|
|
}
|
|
|
|
export async function regenerateInviteCode(roomId: number) {
|
|
return request<{ status: string; room_id: number; invite_code: string }>(
|
|
`/rooms/${roomId}/invite-code`,
|
|
{ method: "POST" },
|
|
)
|
|
}
|
|
|
|
export async function leaveRoom(roomId: number) {
|
|
return request(`/rooms/${roomId}/members/me`, { method: "DELETE" })
|
|
}
|
|
|
|
export async function deleteRoom(roomId: number) {
|
|
return request(`/rooms/${roomId}`, { method: "DELETE" })
|
|
}
|
|
|
|
export async function removeMember(roomId: number, userId: number) {
|
|
return request(`/rooms/${roomId}/members/${userId}`, { method: "DELETE" })
|
|
}
|
|
|
|
export async function updateMemberRole(roomId: number, userId: number, role: RoomRole) {
|
|
return request(`/rooms/${roomId}/members/${userId}`, {
|
|
method: "PATCH",
|
|
body: { role },
|
|
})
|
|
}
|
|
|
|
// ---------- Events ----------
|
|
|
|
export async function getUserEvents(from?: string, to?: string) {
|
|
const res = await request<{ events: CalendarEvent[] }>("/events", {
|
|
query: { from, to },
|
|
})
|
|
return res.events
|
|
}
|
|
|
|
export async function getRoomEvents(roomId: number, from?: string, to?: string) {
|
|
const res = await request<{ events: CalendarEvent[] }>(`/rooms/${roomId}/events`, {
|
|
query: { from, to },
|
|
})
|
|
return res.events
|
|
}
|
|
|
|
export async function getEvent(eventId: number) {
|
|
return request<CalendarEvent>(`/events/${eventId}`)
|
|
}
|
|
|
|
export interface CreateEventInput {
|
|
room_id: number
|
|
type: EventType
|
|
title: string
|
|
description: string
|
|
start_time: string
|
|
end_time: string
|
|
visibility: EventVisibility
|
|
status: EventStatus
|
|
min_people: number
|
|
}
|
|
|
|
export async function createEvent(input: CreateEventInput) {
|
|
return request<{ status: string; event_id: number }>("/events", {
|
|
method: "POST",
|
|
body: input,
|
|
})
|
|
}
|
|
|
|
export async function updateEvent(eventId: number, input: Partial<CreateEventInput>) {
|
|
return request(`/events/${eventId}`, { method: "PATCH", body: input })
|
|
}
|
|
|
|
export async function deleteEvent(eventId: number) {
|
|
return request(`/events/${eventId}`, { method: "DELETE" })
|
|
}
|
|
|
|
export async function confirmEvent(eventId: number) {
|
|
return request(`/events/${eventId}/confirm`, { method: "POST" })
|
|
}
|
|
|
|
export async function cancelEvent(eventId: number) {
|
|
return request(`/events/${eventId}/cancel`, { method: "POST" })
|
|
}
|
|
|
|
export async function respondToEvent(eventId: number, status: SignupStatus) {
|
|
return request<{ status: string; going: number }>(`/events/${eventId}/respond`, {
|
|
method: "POST",
|
|
query: { status },
|
|
})
|
|
}
|
|
|
|
export async function getEventResponses(eventId: number) {
|
|
const res = await request<{ responses: EventResponse[] }>(`/events/${eventId}/responses`)
|
|
return res.responses
|
|
}
|