refactored frontend api helpers to promise interface

This commit is contained in:
Oliver Bryan
2026-01-20 16:59:32 +00:00
parent 11bf3e68f8
commit 45343571f5
31 changed files with 253 additions and 553 deletions

View File

@@ -7,13 +7,26 @@ export * as sprint from "@/lib/server/sprint";
export * as timer from "@/lib/server/timer";
export * as user from "@/lib/server/user";
export type ServerQueryInput<T = unknown> = {
onSuccess?: (data: T, res: Response) => void;
onError?: (error: ApiError | string) => void;
};
export async function getErrorMessage(res: Response, fallback: string): Promise<string> {
const error = await res.json().catch(() => res.text());
if (typeof error === "string") {
return error || fallback;
}
if (error && typeof error === "object") {
if ("details" in error && error.details) {
const messages = Object.values(error.details as Record<string, string[]>).flat();
if (messages.length > 0) return messages.join(", ");
}
if ("error" in error && typeof error.error === "string") {
return error.error || fallback;
}
}
return fallback;
}
export function parseError(error: ApiError | string): string {
export function parseError(error: ApiError | string | Error): string {
if (typeof error === "string") return error;
if (error instanceof Error) return error.message;
if (error.details) {
const messages = Object.values(error.details).flat();
return messages.join(", ");

View File

@@ -1,13 +1,8 @@
import type { IssueResponse } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function byProject({
projectId,
onSuccess,
onError,
}: {
projectId: number;
} & ServerQueryInput) {
export async function byProject(projectId: number): Promise<IssueResponse[]> {
const url = new URL(`${getServerURL()}/issues/by-project`);
url.searchParams.set("projectId", `${projectId}`);
@@ -16,11 +11,9 @@ export async function byProject({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get issues by project (${res.status})`);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to get issues by project (${res.status})`);
throw new Error(message);
}
onSuccess?.(data, res);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { IssueCreateRequest, IssueRecord } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function create(request: IssueCreateRequest & ServerQueryInput<IssueRecord>) {
const { onSuccess, onError, ...body } = request;
export async function create(request: IssueCreateRequest): Promise<IssueRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issue/create`, {
@@ -13,23 +11,18 @@ export async function create(request: IssueCreateRequest & ServerQueryInput<Issu
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to create issue (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to create issue (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as IssueRecord;
if (!data.id) {
toast.error(`failed to create issue (${res.status})`);
onError?.(`failed to create issue (${res.status})`);
return;
}
onSuccess?.(data, res);
throw new Error(`failed to create issue (${res.status})`);
}
return data;
}

View File

@@ -1,15 +1,8 @@
import type { SuccessResponse } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function remove({
issueId,
onSuccess,
onError,
}: {
issueId: number;
} & ServerQueryInput<SuccessResponse>) {
export async function remove(issueId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issue/delete`, {
@@ -23,13 +16,9 @@ export async function remove({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to delete issue (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to delete issue (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,12 +1,8 @@
import type { IssuesReplaceStatusRequest, ReplaceStatusResponse } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function replaceStatus(
request: IssuesReplaceStatusRequest & ServerQueryInput<ReplaceStatusResponse>,
) {
const { onSuccess, onError, ...body } = request;
export async function replaceStatus(request: IssuesReplaceStatusRequest): Promise<ReplaceStatusResponse> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issues/replace-status`, {
@@ -15,18 +11,14 @@ export async function replaceStatus(
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to replace status (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to replace status (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,15 +1,8 @@
import type { StatusCountResponse } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function statusCount({
organisationId,
status,
onSuccess,
onError,
}: {
organisationId: number;
status: string;
} & ServerQueryInput) {
export async function statusCount(organisationId: number, status: string): Promise<StatusCountResponse> {
const url = new URL(`${getServerURL()}/issues/status-count`);
url.searchParams.set("organisationId", `${organisationId}`);
url.searchParams.set("status", status);
@@ -19,10 +12,9 @@ export async function statusCount({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get issue status count (${res.status})`);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get issue status count (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,25 +1,8 @@
import type { IssueRecord } from "@sprint/shared";
import { toast } from "sonner";
import type { IssueRecord, IssueUpdateRequest } from "@sprint/shared";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function update({
issueId,
title,
description,
sprintId,
assigneeIds,
status,
onSuccess,
onError,
}: {
issueId: number;
title?: string;
description?: string;
sprintId?: number | null;
assigneeIds?: number[] | null;
status?: string;
} & ServerQueryInput<IssueRecord>) {
export async function update(input: IssueUpdateRequest): Promise<IssueRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issue/update`, {
@@ -28,25 +11,14 @@ export async function update({
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
id: issueId,
title,
description,
sprintId,
assigneeIds,
status,
}),
body: JSON.stringify(input),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to update issue (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to update issue (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { OrgAddMemberRequest, OrganisationMemberRecord } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function addMember(request: OrgAddMemberRequest & ServerQueryInput<OrganisationMemberRecord>) {
const { onSuccess, onError, ...body } = request;
export async function addMember(request: OrgAddMemberRequest): Promise<OrganisationMemberRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/add-member`, {
@@ -13,18 +11,14 @@ export async function addMember(request: OrgAddMemberRequest & ServerQueryInput<
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to add member (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to add member (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,19 +1,16 @@
import type { OrganisationResponse } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function byUser({ onSuccess, onError }: ServerQueryInput<OrganisationResponse[]>) {
export async function byUser(): Promise<OrganisationResponse[]> {
const res = await fetch(`${getServerURL()}/organisations/by-user`, {
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to get organisations (${res.status})`;
onError?.(message);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get organisations (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { OrganisationRecord, OrgCreateRequest } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function create(request: OrgCreateRequest & ServerQueryInput<OrganisationRecord>) {
const { onSuccess, onError, ...body } = request;
export async function create(request: OrgCreateRequest): Promise<OrganisationRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/create`, {
@@ -13,25 +11,18 @@ export async function create(request: OrgCreateRequest & ServerQueryInput<Organi
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string"
? error
: error.error || `failed to create organisation (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to create organisation (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as OrganisationRecord;
if (!data.id) {
toast.error(`failed to create organisation (${res.status})`);
onError?.(`failed to create organisation (${res.status})`);
return;
}
onSuccess?.(data, res);
throw new Error(`failed to create organisation (${res.status})`);
}
return data;
}

View File

@@ -1,15 +1,8 @@
import type { SuccessResponse } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function remove({
organisationId,
onSuccess,
onError,
}: {
organisationId: number;
} & ServerQueryInput<SuccessResponse>) {
export async function remove(organisationId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/delete`, {
@@ -23,15 +16,9 @@ export async function remove({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string"
? error
: error.error || `failed to delete organisation (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to delete organisation (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,14 +1,8 @@
import type { OrganisationMemberResponse } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function members({
organisationId,
onSuccess,
onError,
}: {
organisationId: number;
} & ServerQueryInput<OrganisationMemberResponse[]>) {
export async function members(organisationId: number): Promise<OrganisationMemberResponse[]> {
const url = new URL(`${getServerURL()}/organisation/members`);
url.searchParams.set("organisationId", `${organisationId}`);
@@ -17,10 +11,9 @@ export async function members({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get members (${res.status})`);
} else {
const data = (await res.json()) as OrganisationMemberResponse[];
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get members (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { OrgRemoveMemberRequest, SuccessResponse } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function removeMember(request: OrgRemoveMemberRequest & ServerQueryInput<SuccessResponse>) {
const { onSuccess, onError, ...body } = request;
export async function removeMember(request: OrgRemoveMemberRequest): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/remove-member`, {
@@ -13,18 +11,14 @@ export async function removeMember(request: OrgRemoveMemberRequest & ServerQuery
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to remove member (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to remove member (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,23 +1,8 @@
import type { OrganisationRecord } from "@sprint/shared";
import { toast } from "sonner";
import type { OrganisationRecord, OrgUpdateRequest } from "@sprint/shared";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function update({
organisationId,
name,
description,
slug,
statuses,
onSuccess,
onError,
}: {
organisationId: number;
name?: string;
description?: string;
slug?: string;
statuses?: Record<string, string>;
} & ServerQueryInput<OrganisationRecord>) {
export async function update(input: OrgUpdateRequest): Promise<OrganisationRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/update`, {
@@ -26,26 +11,14 @@ export async function update({
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
id: organisationId,
name,
description,
slug,
statuses,
}),
body: JSON.stringify(input),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string"
? error
: error.error || `failed to update organisation (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to update organisation (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,12 +1,10 @@
import type { OrganisationMemberRecord, OrgUpdateMemberRoleRequest } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function updateMemberRole(
request: OrgUpdateMemberRoleRequest & ServerQueryInput<OrganisationMemberRecord>,
) {
const { onSuccess, onError, ...body } = request;
request: OrgUpdateMemberRoleRequest,
): Promise<OrganisationMemberRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/update-member-role`, {
@@ -15,18 +13,14 @@ export async function updateMemberRole(
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to update member role (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to update member role (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,13 +1,8 @@
import type { ProjectResponse } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function byOrganisation({
organisationId,
onSuccess,
onError,
}: {
organisationId: number;
} & ServerQueryInput) {
export async function byOrganisation(organisationId: number): Promise<ProjectResponse[]> {
const url = new URL(`${getServerURL()}/projects/by-organisation`);
url.searchParams.set("organisationId", `${organisationId}`);
@@ -16,11 +11,9 @@ export async function byOrganisation({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get projects by organisation (${res.status})`);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to get projects by organisation (${res.status})`);
throw new Error(message);
}
onSuccess?.(data, res);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { ProjectCreateRequest, ProjectRecord } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function create(request: ProjectCreateRequest & ServerQueryInput<ProjectRecord>) {
const { onSuccess, onError, ...body } = request;
export async function create(request: ProjectCreateRequest): Promise<ProjectRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/project/create`, {
@@ -13,23 +11,18 @@ export async function create(request: ProjectCreateRequest & ServerQueryInput<Pr
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to create project (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to create project (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as ProjectRecord;
if (!data.id) {
toast.error(`failed to create project (${res.status})`);
onError?.(`failed to create project (${res.status})`);
return;
}
onSuccess?.(data, res);
throw new Error(`failed to create project (${res.status})`);
}
return data;
}

View File

@@ -1,15 +1,8 @@
import type { SuccessResponse } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function remove({
projectId,
onSuccess,
onError,
}: {
projectId: number;
} & ServerQueryInput<SuccessResponse>) {
export async function remove(projectId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/project/delete`, {
@@ -23,13 +16,9 @@ export async function remove({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to delete project (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to delete project (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,19 +1,8 @@
import type { ProjectRecord } from "@sprint/shared";
import { toast } from "sonner";
import type { ProjectRecord, ProjectUpdateRequest } from "@sprint/shared";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function update({
projectId,
key,
name,
onSuccess,
onError,
}: {
projectId: number;
key?: string;
name?: string;
} & ServerQueryInput<ProjectRecord>) {
export async function update(input: ProjectUpdateRequest): Promise<ProjectRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/project/update`, {
@@ -22,22 +11,14 @@ export async function update({
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
id: projectId,
key,
name,
}),
body: JSON.stringify(input),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to update project (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to update project (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,14 +1,8 @@
import type { SprintRecord } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function byProject({
projectId,
onSuccess,
onError,
}: {
projectId: number;
} & ServerQueryInput<SprintRecord[]>) {
export async function byProject(projectId: number): Promise<SprintRecord[]> {
const url = new URL(`${getServerURL()}/sprints/by-project`);
url.searchParams.set("projectId", `${projectId}`);
@@ -17,10 +11,9 @@ export async function byProject({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get sprints (${res.status})`);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get sprints (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,23 +1,8 @@
import type { SprintRecord } from "@sprint/shared";
import { toast } from "sonner";
import type { SprintCreateRequest, SprintRecord } from "@sprint/shared";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function create({
projectId,
name,
color,
startDate,
endDate,
onSuccess,
onError,
}: {
projectId: number;
name: string;
color?: string;
startDate: Date;
endDate: Date;
} & ServerQueryInput<SprintRecord>) {
export async function create(input: SprintCreateRequest): Promise<SprintRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/sprint/create`, {
@@ -27,28 +12,20 @@ export async function create({
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
projectId,
name: name.trim(),
color,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
...input,
name: input.name.trim(),
}),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to create sprint (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to create sprint (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as SprintRecord;
if (!data.id) {
toast.error(`failed to create sprint (${res.status})`);
onError?.(`failed to create sprint (${res.status})`);
return;
}
onSuccess?.(data, res);
throw new Error(`failed to create sprint (${res.status})`);
}
return data;
}

View File

@@ -1,15 +1,8 @@
import type { SuccessResponse } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function remove({
sprintId,
onSuccess,
onError,
}: {
sprintId: number;
} & ServerQueryInput<SuccessResponse>) {
export async function remove(sprintId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/sprint/delete`, {
@@ -23,13 +16,9 @@ export async function remove({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to delete sprint (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to delete sprint (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,23 +1,8 @@
import type { SprintRecord } from "@sprint/shared";
import { toast } from "sonner";
import type { SprintRecord, SprintUpdateRequest } from "@sprint/shared";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function update({
sprintId,
name,
color,
startDate,
endDate,
onSuccess,
onError,
}: {
sprintId: number;
name?: string;
color?: string;
startDate?: Date;
endDate?: Date;
} & ServerQueryInput<SprintRecord>) {
export async function update(input: SprintUpdateRequest): Promise<SprintRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/sprint/update`, {
@@ -27,23 +12,16 @@ export async function update({
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
id: sprintId,
name: name?.trim(),
color,
startDate: startDate?.toISOString(),
endDate: endDate?.toISOString(),
...input,
name: input.name?.trim(),
}),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to update sprint (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to update sprint (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { TimerEndRequest, TimerState } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function end(request: TimerEndRequest & ServerQueryInput<TimerState>) {
const { onSuccess, onError, ...body } = request;
export async function end(request: TimerEndRequest): Promise<TimerState> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/timer/end`, {
@@ -13,18 +11,14 @@ export async function end(request: TimerEndRequest & ServerQueryInput<TimerState
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to end timer (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to end timer (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,14 +1,8 @@
import type { TimerState } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function get({
issueId,
onSuccess,
onError,
}: {
issueId: number;
} & ServerQueryInput<TimerState>) {
export async function get(issueId: number): Promise<TimerState> {
const url = new URL(`${getServerURL()}/timer/get`);
url.searchParams.set("issueId", `${issueId}`);
@@ -17,12 +11,9 @@ export async function get({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to get timer (${res.status})`;
onError?.(message);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get timer (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,14 +1,8 @@
import type { TimerState } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function getInactive({
issueId,
onSuccess,
onError,
}: {
issueId: number;
} & ServerQueryInput<TimerState[]>) {
export async function getInactive(issueId: number): Promise<TimerState[]> {
const url = new URL(`${getServerURL()}/timer/get-inactive`);
url.searchParams.set("issueId", `${issueId}`);
@@ -17,12 +11,10 @@ export async function getInactive({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to get timers (${res.status})`;
onError?.(message);
} else {
const data = await res.json();
onSuccess?.(data || [], res);
const message = await getErrorMessage(res, `failed to get timers (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as TimerState[];
return data ?? [];
}

View File

@@ -1,18 +1,15 @@
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function list({
limit,
offset,
onSuccess,
onError,
}: {
type TimerListInput = {
limit?: number;
offset?: number;
} & ServerQueryInput) {
};
export async function list(input: TimerListInput = {}): Promise<unknown> {
const url = new URL(`${getServerURL()}/timers`);
if (limit != null) url.searchParams.set("limit", `${limit}`);
if (offset != null) url.searchParams.set("offset", `${offset}`);
if (input.limit != null) url.searchParams.set("limit", `${input.limit}`);
if (input.offset != null) url.searchParams.set("offset", `${input.offset}`);
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
@@ -24,10 +21,9 @@ export async function list({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get timers (${res.status})`);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get timers (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { TimerState, TimerToggleRequest } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function toggle(request: TimerToggleRequest & ServerQueryInput<TimerState>) {
const { onSuccess, onError, ...body } = request;
export async function toggle(request: TimerToggleRequest): Promise<TimerState> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/timer/toggle`, {
@@ -13,18 +11,14 @@ export async function toggle(request: TimerToggleRequest & ServerQueryInput<Time
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to toggle timer (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to toggle timer (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,14 +1,8 @@
import type { UserRecord } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function byUsername({
username,
onSuccess,
onError,
}: {
username: string;
} & ServerQueryInput<UserRecord>) {
export async function byUsername(username: string): Promise<UserRecord> {
const url = new URL(`${getServerURL()}/user/by-username`);
url.searchParams.set("username", username);
@@ -17,12 +11,9 @@ export async function byUsername({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to get user (${res.status})`;
onError?.(message);
} else {
const data = (await res.json()) as UserRecord;
onSuccess?.(data, res);
const message = await getErrorMessage(res, `failed to get user (${res.status})`);
throw new Error(message);
}
return res.json();
}

View File

@@ -1,10 +1,8 @@
import type { UserRecord, UserUpdateRequest } from "@sprint/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function update(request: UserUpdateRequest & ServerQueryInput<UserRecord>) {
const { onSuccess, onError, ...body } = request;
export async function update(request: UserUpdateRequest): Promise<UserRecord> {
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/user/update`, {
@@ -13,23 +11,18 @@ export async function update(request: UserUpdateRequest & ServerQueryInput<UserR
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `failed to update user (${res.status})`;
toast.error(message);
onError?.(error);
} else {
const data = await res.json();
const message = await getErrorMessage(res, `failed to update user (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as UserRecord;
if (!data.id) {
toast.error(`failed to update user (${res.status})`);
onError?.(`failed to update user (${res.status})`);
return;
}
onSuccess?.(data, res);
throw new Error(`failed to update user (${res.status})`);
}
return data;
}

View File

@@ -1,24 +1,16 @@
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
import { getErrorMessage } from "..";
export async function uploadAvatar({
file,
onSuccess,
onError,
}: {
file: File;
} & ServerQueryInput<string>) {
export async function uploadAvatar(file: File): Promise<string> {
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
if (file.size > MAX_FILE_SIZE) {
onError?.("File size exceeds 5MB limit");
return;
throw new Error("File size exceeds 5MB limit");
}
if (!ALLOWED_TYPES.includes(file.type)) {
onError?.("Invalid file type. Allowed types: png, jpg, jpeg, webp, gif");
return;
throw new Error("Invalid file type. Allowed types: png, jpg, jpeg, webp, gif");
}
const formData = new FormData();
@@ -36,17 +28,14 @@ export async function uploadAvatar({
});
if (!res.ok) {
const error = await res.json().catch(() => res.text());
const message =
typeof error === "string" ? error : error.error || `Failed to upload avatar (${res.status})`;
onError?.(message);
return;
const message = await getErrorMessage(res, `Failed to upload avatar (${res.status})`);
throw new Error(message);
}
const data = await res.json();
if (data.avatarURL) {
onSuccess?.(data.avatarURL, res);
} else {
onError?.("Failed to upload avatar");
return data.avatarURL;
}
throw new Error("Failed to upload avatar");
}