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 } async function request(path: string, options: RequestOptions = {}): Promise { 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 = {} 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("/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(`/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(`/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) { 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 }