use ServerQueryInput properly

This commit is contained in:
Oliver Bryan
2026-01-13 15:33:55 +00:00
parent 4e93eb5878
commit ca371b1751
21 changed files with 284 additions and 329 deletions

View File

@@ -1,50 +1,35 @@
import type { IssueCreateRequest, IssueRecord } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function create({
projectId,
title,
description,
sprintId,
assigneeId,
status,
onSuccess,
onError,
}: {
projectId: number;
title: string;
description: string;
sprintId?: number | null;
assigneeId?: number | null;
status?: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issue/create`);
url.searchParams.set("projectId", `${projectId}`);
url.searchParams.set("title", title.trim());
if (description.trim() !== "") url.searchParams.set("description", description.trim());
if (sprintId != null) url.searchParams.set("sprintId", `${sprintId}`);
if (assigneeId != null) url.searchParams.set("assigneeId", `${assigneeId}`);
if (status != null && status.trim() !== "") url.searchParams.set("status", status.trim());
export async function create(request: IssueCreateRequest & ServerQueryInput<IssueRecord>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/issue/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create issue (${res.status})`);
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();
if (!data.id) {
toast.error(`failed to create issue (${res.status})`);
onError?.(`failed to create issue (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -1,3 +1,5 @@
import type { SuccessResponse } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
@@ -7,24 +9,27 @@ export async function remove({
onError,
}: {
issueId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issue/delete`);
url.searchParams.set("id", `${issueId}`);
} & ServerQueryInput<SuccessResponse>) {
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/issue/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: issueId }),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to delete issue (${res.status})`);
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.text();
const data = await res.json();
onSuccess?.(data, res);
}
}

View File

@@ -1,34 +1,30 @@
import type { IssuesReplaceStatusRequest, ReplaceStatusResponse } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function replaceStatus({
organisationId,
oldStatus,
newStatus,
onSuccess,
onError,
}: {
organisationId: number;
oldStatus: string;
newStatus: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issues/replace-status`);
url.searchParams.set("organisationId", `${organisationId}`);
url.searchParams.set("oldStatus", oldStatus);
url.searchParams.set("newStatus", newStatus);
export async function replaceStatus(
request: IssuesReplaceStatusRequest & ServerQueryInput<ReplaceStatusResponse>,
) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/issues/replace-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to replace status (${res.status})`);
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);

View File

@@ -1,3 +1,5 @@
import type { IssueRecord } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
@@ -17,31 +19,32 @@ export async function update({
sprintId?: number | null;
assigneeId?: number | null;
status?: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issue/update`);
url.searchParams.set("id", `${issueId}`);
if (title !== undefined) url.searchParams.set("title", title);
if (description !== undefined) url.searchParams.set("description", description);
if (sprintId !== undefined) {
url.searchParams.set("sprintId", sprintId === null ? "null" : `${sprintId}`);
}
if (assigneeId !== undefined) {
url.searchParams.set("assigneeId", assigneeId === null ? "null" : `${assigneeId}`);
}
if (status !== undefined) url.searchParams.set("status", status);
} & ServerQueryInput<IssueRecord>) {
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/issue/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
id: issueId,
title,
description,
sprintId,
assigneeId,
status,
}),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to update issue (${res.status})`);
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);

View File

@@ -1,35 +1,28 @@
import type { OrgAddMemberRequest, OrganisationMemberRecord } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function addMember({
organisationId,
userId,
role = "member",
onSuccess,
onError,
}: {
organisationId: number;
userId: number;
role?: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/add-member`);
url.searchParams.set("organisationId", `${organisationId}`);
url.searchParams.set("userId", `${userId}`);
url.searchParams.set("role", role);
export async function addMember(request: OrgAddMemberRequest & ServerQueryInput<OrganisationMemberRecord>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
const res = await fetch(`${getServerURL()}/organisation/add-member`, {
method: "POST",
headers,
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to add member (${res.status})`);
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);

View File

@@ -1,23 +1,17 @@
import type { OrganisationResponse } from "@issue/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function byUser({
userId,
onSuccess,
onError,
}: {
userId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisations/by-user`);
url.searchParams.set("userId", `${userId}`);
const res = await fetch(url.toString(), {
export async function byUser({ onSuccess, onError }: ServerQueryInput<OrganisationResponse[]>) {
const res = await fetch(`${getServerURL()}/organisations/by-user`, {
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get organisations (${res.status})`);
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);

View File

@@ -1,44 +1,37 @@
import type { OrganisationRecord, OrgCreateRequest } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function create({
name,
slug,
userId,
description,
onSuccess,
onError,
}: {
name: string;
slug: string;
userId: number;
description: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/create`);
url.searchParams.set("name", name.trim());
url.searchParams.set("slug", slug.trim());
url.searchParams.set("userId", `${userId}`);
if (description.trim() !== "") url.searchParams.set("description", description.trim());
export async function create(request: OrgCreateRequest & ServerQueryInput<OrganisationRecord>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/organisation/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create organisation (${res.status})`);
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();
if (!data.id) {
toast.error(`failed to create organisation (${res.status})`);
onError?.(`failed to create organisation (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -8,7 +8,7 @@ export async function members({
onError,
}: {
organisationId: number;
} & ServerQueryInput) {
} & ServerQueryInput<OrganisationMemberResponse[]>) {
const url = new URL(`${getServerURL()}/organisation/members`);
url.searchParams.set("organisationId", `${organisationId}`);

View File

@@ -1,32 +1,28 @@
import type { OrgRemoveMemberRequest, SuccessResponse } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function removeMember({
organisationId,
userId,
onSuccess,
onError,
}: {
organisationId: number;
userId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/remove-member`);
url.searchParams.set("organisationId", `${organisationId}`);
url.searchParams.set("userId", `${userId}`);
export async function removeMember(request: OrgRemoveMemberRequest & ServerQueryInput<SuccessResponse>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
const res = await fetch(`${getServerURL()}/organisation/remove-member`, {
method: "POST",
headers,
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to remove member (${res.status})`);
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);

View File

@@ -1,3 +1,5 @@
import type { OrganisationRecord } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
@@ -15,28 +17,33 @@ export async function update({
description?: string;
slug?: string;
statuses?: Record<string, string>;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/update`);
url.searchParams.set("id", `${organisationId}`);
if (name !== undefined) url.searchParams.set("name", name);
if (description !== undefined) url.searchParams.set("description", description);
if (slug !== undefined) url.searchParams.set("slug", slug);
if (statuses !== undefined) {
url.searchParams.set("statuses", JSON.stringify(statuses));
}
} & ServerQueryInput<OrganisationRecord>) {
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/organisation/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
id: organisationId,
name,
description,
slug,
statuses,
}),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to update organisation (${res.status})`);
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);

View File

@@ -1,35 +1,30 @@
import type { OrganisationMemberRecord, OrgUpdateMemberRoleRequest } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function updateMemberRole({
organisationId,
userId,
role,
onSuccess,
onError,
}: {
organisationId: number;
userId: number;
role: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/update-member-role`);
url.searchParams.set("organisationId", `${organisationId}`);
url.searchParams.set("userId", `${userId}`);
url.searchParams.set("role", role);
export async function updateMemberRole(
request: OrgUpdateMemberRoleRequest & ServerQueryInput<OrganisationMemberRecord>,
) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
const res = await fetch(`${getServerURL()}/organisation/update-member-role`, {
method: "POST",
headers,
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to update member role (${res.status})`);
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);

View File

@@ -1,44 +1,35 @@
import type { ProjectCreateRequest, ProjectRecord } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function create({
key,
name,
creatorId,
organisationId,
onSuccess,
onError,
}: {
key: string;
name: string;
creatorId: number;
organisationId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/project/create`);
url.searchParams.set("key", key.trim());
url.searchParams.set("name", name.trim());
url.searchParams.set("creatorId", `${creatorId}`);
url.searchParams.set("organisationId", `${organisationId}`);
export async function create(request: ProjectCreateRequest & ServerQueryInput<ProjectRecord>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/project/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create project (${res.status})`);
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();
if (!data.id) {
toast.error(`failed to create project (${res.status})`);
onError?.(`failed to create project (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -1,3 +1,4 @@
import type { SprintRecord } from "@issue/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
@@ -7,7 +8,7 @@ export async function byProject({
onError,
}: {
projectId: number;
} & ServerQueryInput) {
} & ServerQueryInput<SprintRecord[]>) {
const url = new URL(`${getServerURL()}/sprints/by-project`);
url.searchParams.set("projectId", `${projectId}`);

View File

@@ -1,3 +1,5 @@
import type { SprintRecord } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
@@ -12,36 +14,41 @@ export async function create({
}: {
projectId: number;
name: string;
color: string;
color?: string;
startDate: Date;
endDate: Date;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/sprint/create`);
url.searchParams.set("projectId", `${projectId}`);
url.searchParams.set("name", name.trim());
url.searchParams.set("color", color);
url.searchParams.set("startDate", startDate.toISOString());
url.searchParams.set("endDate", endDate.toISOString());
} & ServerQueryInput<SprintRecord>) {
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/sprint/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
projectId,
name: name.trim(),
color,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
}),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create sprint (${res.status})`);
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();
if (!data.id) {
toast.error(`failed to create sprint (${res.status})`);
onError?.(`failed to create sprint (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -1,29 +1,28 @@
import type { TimerEndRequest, TimerState } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function end({
issueId,
onSuccess,
onError,
}: {
issueId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/timer/end`);
url.searchParams.set("issueId", `${issueId}`);
export async function end(request: TimerEndRequest & ServerQueryInput<TimerState>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
const res = await fetch(`${getServerURL()}/timer/end`, {
method: "POST",
headers,
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to end timer (${res.status})`);
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);

View File

@@ -1,4 +1,5 @@
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { TimerState } from "@issue/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function get({
@@ -7,22 +8,19 @@ export async function get({
onError,
}: {
issueId: number;
} & ServerQueryInput) {
} & ServerQueryInput<TimerState>) {
const url = new URL(`${getServerURL()}/timer/get`);
url.searchParams.set("issueId", `${issueId}`);
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get timer (${res.status})`);
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);

View File

@@ -1,4 +1,5 @@
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { TimerState } from "@issue/shared";
import { getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function getInactive({
@@ -7,24 +8,21 @@ export async function getInactive({
onError,
}: {
issueId: number;
} & ServerQueryInput) {
} & ServerQueryInput<TimerState[]>) {
const url = new URL(`${getServerURL()}/timer/get-inactive`);
url.searchParams.set("issueId", `${issueId}`);
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get timers (${res.status})`);
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);
onSuccess?.(data || [], res);
}
}

View File

@@ -1,29 +1,28 @@
import type { TimerState, TimerToggleRequest } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function toggle({
issueId,
onSuccess,
onError,
}: {
issueId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/timer/toggle`);
url.searchParams.set("issueId", `${issueId}`);
export async function toggle(request: TimerToggleRequest & ServerQueryInput<TimerState>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
const res = await fetch(`${getServerURL()}/timer/toggle`, {
method: "POST",
headers,
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to toggle timer (${res.status})`);
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);

View File

@@ -8,7 +8,7 @@ export async function byUsername({
onError,
}: {
username: string;
} & ServerQueryInput) {
} & ServerQueryInput<UserRecord>) {
const url = new URL(`${getServerURL()}/user/by-username`);
url.searchParams.set("username", username);
@@ -17,8 +17,10 @@ export async function byUsername({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get user (${res.status})`);
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);

View File

@@ -1,44 +1,35 @@
import type { UserRecord, UserUpdateRequest } from "@issue/shared";
import { toast } from "sonner";
import { getCsrfToken, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function update({
id,
name,
password,
avatarURL,
onSuccess,
onError,
}: {
id: number;
name: string;
password: string;
avatarURL: string | null;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/user/update`);
url.searchParams.set("id", `${id}`);
url.searchParams.set("name", name.trim());
url.searchParams.set("password", password.trim());
url.searchParams.set("avatarURL", avatarURL || "null");
export async function update(request: UserUpdateRequest & ServerQueryInput<UserRecord>) {
const { onSuccess, onError, ...body } = request;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
const res = await fetch(`${getServerURL()}/user/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(body),
credentials: "include",
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to update user (${res.status})`);
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();
if (!data.id) {
toast.error(`failed to update user (${res.status})`);
onError?.(`failed to update user (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -7,7 +7,7 @@ export async function uploadAvatar({
onError,
}: {
file: File;
} & ServerQueryInput) {
} & ServerQueryInput<string>) {
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
@@ -36,8 +36,10 @@ export async function uploadAvatar({
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `Failed to upload avatar (${res.status})`);
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;
}