From ca371b1751f76ce9f79ac113025a71bce8d514f0 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Tue, 13 Jan 2026 15:33:55 +0000 Subject: [PATCH] use ServerQueryInput properly --- .../frontend/src/lib/server/issue/create.ts | 49 +++++++------------ .../frontend/src/lib/server/issue/delete.ts | 27 +++++----- .../src/lib/server/issue/replaceStatus.ts | 40 +++++++-------- .../frontend/src/lib/server/issue/update.ts | 41 +++++++++------- .../src/lib/server/organisation/addMember.ts | 37 ++++++-------- .../src/lib/server/organisation/byUser.ts | 20 +++----- .../src/lib/server/organisation/create.ts | 45 +++++++---------- .../src/lib/server/organisation/members.ts | 2 +- .../lib/server/organisation/removeMember.ts | 34 ++++++------- .../src/lib/server/organisation/update.ts | 39 +++++++++------ .../server/organisation/updateMemberRole.ts | 39 +++++++-------- .../frontend/src/lib/server/project/create.ts | 43 +++++++--------- .../src/lib/server/sprint/byProject.ts | 3 +- .../frontend/src/lib/server/sprint/create.ts | 39 +++++++++------ packages/frontend/src/lib/server/timer/end.ts | 31 ++++++------ packages/frontend/src/lib/server/timer/get.ts | 16 +++--- .../src/lib/server/timer/getInactive.ts | 18 +++---- .../frontend/src/lib/server/timer/toggle.ts | 31 ++++++------ .../src/lib/server/user/byUsername.ts | 8 +-- .../frontend/src/lib/server/user/update.ts | 43 +++++++--------- .../src/lib/server/user/uploadAvatar.ts | 8 +-- 21 files changed, 284 insertions(+), 329 deletions(-) diff --git a/packages/frontend/src/lib/server/issue/create.ts b/packages/frontend/src/lib/server/issue/create.ts index 9c62ec3..2fadb11 100644 --- a/packages/frontend/src/lib/server/issue/create.ts +++ b/packages/frontend/src/lib/server/issue/create.ts @@ -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) { + 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); } } diff --git a/packages/frontend/src/lib/server/issue/delete.ts b/packages/frontend/src/lib/server/issue/delete.ts index 9d3ff15..ca7106e 100644 --- a/packages/frontend/src/lib/server/issue/delete.ts +++ b/packages/frontend/src/lib/server/issue/delete.ts @@ -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) { 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); } } diff --git a/packages/frontend/src/lib/server/issue/replaceStatus.ts b/packages/frontend/src/lib/server/issue/replaceStatus.ts index 58f3e2e..f8b8b37 100644 --- a/packages/frontend/src/lib/server/issue/replaceStatus.ts +++ b/packages/frontend/src/lib/server/issue/replaceStatus.ts @@ -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, +) { + 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); diff --git a/packages/frontend/src/lib/server/issue/update.ts b/packages/frontend/src/lib/server/issue/update.ts index 49f6fe8..b9563ff 100644 --- a/packages/frontend/src/lib/server/issue/update.ts +++ b/packages/frontend/src/lib/server/issue/update.ts @@ -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) { 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); diff --git a/packages/frontend/src/lib/server/organisation/addMember.ts b/packages/frontend/src/lib/server/organisation/addMember.ts index 78610eb..b1792ef 100644 --- a/packages/frontend/src/lib/server/organisation/addMember.ts +++ b/packages/frontend/src/lib/server/organisation/addMember.ts @@ -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) { + 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); diff --git a/packages/frontend/src/lib/server/organisation/byUser.ts b/packages/frontend/src/lib/server/organisation/byUser.ts index 5b9fe83..f0f69ea 100644 --- a/packages/frontend/src/lib/server/organisation/byUser.ts +++ b/packages/frontend/src/lib/server/organisation/byUser.ts @@ -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) { + 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); diff --git a/packages/frontend/src/lib/server/organisation/create.ts b/packages/frontend/src/lib/server/organisation/create.ts index bba571b..5359d20 100644 --- a/packages/frontend/src/lib/server/organisation/create.ts +++ b/packages/frontend/src/lib/server/organisation/create.ts @@ -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) { + 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); } } diff --git a/packages/frontend/src/lib/server/organisation/members.ts b/packages/frontend/src/lib/server/organisation/members.ts index e8845f7..7051afb 100644 --- a/packages/frontend/src/lib/server/organisation/members.ts +++ b/packages/frontend/src/lib/server/organisation/members.ts @@ -8,7 +8,7 @@ export async function members({ onError, }: { organisationId: number; -} & ServerQueryInput) { +} & ServerQueryInput) { const url = new URL(`${getServerURL()}/organisation/members`); url.searchParams.set("organisationId", `${organisationId}`); diff --git a/packages/frontend/src/lib/server/organisation/removeMember.ts b/packages/frontend/src/lib/server/organisation/removeMember.ts index 27d8ad0..50e9e22 100644 --- a/packages/frontend/src/lib/server/organisation/removeMember.ts +++ b/packages/frontend/src/lib/server/organisation/removeMember.ts @@ -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) { + 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); diff --git a/packages/frontend/src/lib/server/organisation/update.ts b/packages/frontend/src/lib/server/organisation/update.ts index 5eea92d..b8986fb 100644 --- a/packages/frontend/src/lib/server/organisation/update.ts +++ b/packages/frontend/src/lib/server/organisation/update.ts @@ -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; -} & 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) { 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); diff --git a/packages/frontend/src/lib/server/organisation/updateMemberRole.ts b/packages/frontend/src/lib/server/organisation/updateMemberRole.ts index 096ace2..5e10a2d 100644 --- a/packages/frontend/src/lib/server/organisation/updateMemberRole.ts +++ b/packages/frontend/src/lib/server/organisation/updateMemberRole.ts @@ -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, +) { + 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); diff --git a/packages/frontend/src/lib/server/project/create.ts b/packages/frontend/src/lib/server/project/create.ts index b8ee893..090fbba 100644 --- a/packages/frontend/src/lib/server/project/create.ts +++ b/packages/frontend/src/lib/server/project/create.ts @@ -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) { + 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); } } diff --git a/packages/frontend/src/lib/server/sprint/byProject.ts b/packages/frontend/src/lib/server/sprint/byProject.ts index b93a24d..4e97013 100644 --- a/packages/frontend/src/lib/server/sprint/byProject.ts +++ b/packages/frontend/src/lib/server/sprint/byProject.ts @@ -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) { const url = new URL(`${getServerURL()}/sprints/by-project`); url.searchParams.set("projectId", `${projectId}`); diff --git a/packages/frontend/src/lib/server/sprint/create.ts b/packages/frontend/src/lib/server/sprint/create.ts index 136c609..ec8238b 100644 --- a/packages/frontend/src/lib/server/sprint/create.ts +++ b/packages/frontend/src/lib/server/sprint/create.ts @@ -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) { 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); } } diff --git a/packages/frontend/src/lib/server/timer/end.ts b/packages/frontend/src/lib/server/timer/end.ts index 863fbaf..f86d975 100644 --- a/packages/frontend/src/lib/server/timer/end.ts +++ b/packages/frontend/src/lib/server/timer/end.ts @@ -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) { + 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); diff --git a/packages/frontend/src/lib/server/timer/get.ts b/packages/frontend/src/lib/server/timer/get.ts index 2357146..91e38f4 100644 --- a/packages/frontend/src/lib/server/timer/get.ts +++ b/packages/frontend/src/lib/server/timer/get.ts @@ -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) { 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); diff --git a/packages/frontend/src/lib/server/timer/getInactive.ts b/packages/frontend/src/lib/server/timer/getInactive.ts index ada001b..9d7a43e 100644 --- a/packages/frontend/src/lib/server/timer/getInactive.ts +++ b/packages/frontend/src/lib/server/timer/getInactive.ts @@ -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) { 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); } } diff --git a/packages/frontend/src/lib/server/timer/toggle.ts b/packages/frontend/src/lib/server/timer/toggle.ts index ef650e7..820a9da 100644 --- a/packages/frontend/src/lib/server/timer/toggle.ts +++ b/packages/frontend/src/lib/server/timer/toggle.ts @@ -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) { + 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); diff --git a/packages/frontend/src/lib/server/user/byUsername.ts b/packages/frontend/src/lib/server/user/byUsername.ts index d245925..fcb20e1 100644 --- a/packages/frontend/src/lib/server/user/byUsername.ts +++ b/packages/frontend/src/lib/server/user/byUsername.ts @@ -8,7 +8,7 @@ export async function byUsername({ onError, }: { username: string; -} & ServerQueryInput) { +} & ServerQueryInput) { 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); diff --git a/packages/frontend/src/lib/server/user/update.ts b/packages/frontend/src/lib/server/user/update.ts index 00713f4..ce86587 100644 --- a/packages/frontend/src/lib/server/user/update.ts +++ b/packages/frontend/src/lib/server/user/update.ts @@ -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) { + 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); } } diff --git a/packages/frontend/src/lib/server/user/uploadAvatar.ts b/packages/frontend/src/lib/server/user/uploadAvatar.ts index 33cd2b5..3c754db 100644 --- a/packages/frontend/src/lib/server/user/uploadAvatar.ts +++ b/packages/frontend/src/lib/server/user/uploadAvatar.ts @@ -7,7 +7,7 @@ export async function uploadAvatar({ onError, }: { file: File; -} & ServerQueryInput) { +} & ServerQueryInput) { 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; }