frontend indentation set to 2

This commit is contained in:
Oliver Bryan
2026-01-21 17:47:04 +00:00
parent 70504b3056
commit 5a5e40659c
117 changed files with 7548 additions and 7785 deletions

View File

@@ -1,16 +1,16 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 mins
gcTime: 10 * 60 * 1000, // 10 mins
retry: 1,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
mutations: {
retry: 0,
},
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 mins
gcTime: 10 * 60 * 1000, // 10 mins
retry: 1,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
mutations: {
retry: 0,
},
},
});

View File

@@ -5,31 +5,31 @@ import { useOrganisations } from "@/lib/query/hooks/organisations";
import { useProjects } from "@/lib/query/hooks/projects";
export function useSelectedOrganisation() {
const { selectedOrganisationId } = useSelection();
const { data: organisations = [] } = useOrganisations();
const { selectedOrganisationId } = useSelection();
const { data: organisations = [] } = useOrganisations();
return useMemo(
() => organisations.find((org) => org.Organisation.id === selectedOrganisationId) ?? null,
[organisations, selectedOrganisationId],
);
return useMemo(
() => organisations.find((org) => org.Organisation.id === selectedOrganisationId) ?? null,
[organisations, selectedOrganisationId],
);
}
export function useSelectedProject() {
const { selectedOrganisationId, selectedProjectId } = useSelection();
const { data: projects = [] } = useProjects(selectedOrganisationId);
const { selectedOrganisationId, selectedProjectId } = useSelection();
const { data: projects = [] } = useProjects(selectedOrganisationId);
return useMemo(
() => projects.find((project) => project.Project.id === selectedProjectId) ?? null,
[projects, selectedProjectId],
);
return useMemo(
() => projects.find((project) => project.Project.id === selectedProjectId) ?? null,
[projects, selectedProjectId],
);
}
export function useSelectedIssue() {
const { selectedProjectId, selectedIssueId } = useSelection();
const { data: issues = [] } = useIssues(selectedProjectId);
const { selectedProjectId, selectedIssueId } = useSelection();
const { data: issues = [] } = useIssues(selectedProjectId);
return useMemo(
() => issues.find((issue) => issue.Issue.id === selectedIssueId) ?? null,
[issues, selectedIssueId],
);
return useMemo(
() => issues.find((issue) => issue.Issue.id === selectedIssueId) ?? null,
[issues, selectedIssueId],
);
}

View File

@@ -1,78 +1,78 @@
import type {
IssueCreateRequest,
IssueRecord,
IssueResponse,
IssuesReplaceStatusRequest,
IssueUpdateRequest,
StatusCountResponse,
SuccessResponse,
IssueCreateRequest,
IssueRecord,
IssueResponse,
IssuesReplaceStatusRequest,
IssueUpdateRequest,
StatusCountResponse,
SuccessResponse,
} from "@sprint/shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query/keys";
import { issue } from "@/lib/server";
export function useIssues(projectId?: number | null) {
return useQuery<IssueResponse[]>({
queryKey: queryKeys.issues.byProject(projectId ?? 0),
queryFn: () => issue.byProject(projectId ?? 0),
enabled: Boolean(projectId),
});
return useQuery<IssueResponse[]>({
queryKey: queryKeys.issues.byProject(projectId ?? 0),
queryFn: () => issue.byProject(projectId ?? 0),
enabled: Boolean(projectId),
});
}
export function useIssueStatusCount(organisationId?: number | null, status?: string | null) {
return useQuery<StatusCountResponse>({
queryKey: queryKeys.issues.statusCount(organisationId ?? 0, status ?? ""),
queryFn: () => issue.statusCount(organisationId ?? 0, status ?? ""),
enabled: Boolean(organisationId && status),
});
return useQuery<StatusCountResponse>({
queryKey: queryKeys.issues.statusCount(organisationId ?? 0, status ?? ""),
queryFn: () => issue.statusCount(organisationId ?? 0, status ?? ""),
enabled: Boolean(organisationId && status),
});
}
export function useCreateIssue() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<IssueRecord, Error, IssueCreateRequest>({
mutationKey: ["issues", "create"],
mutationFn: issue.create,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.issues.byProject(variables.projectId),
});
},
});
return useMutation<IssueRecord, Error, IssueCreateRequest>({
mutationKey: ["issues", "create"],
mutationFn: issue.create,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.issues.byProject(variables.projectId),
});
},
});
}
export function useUpdateIssue() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<IssueRecord, Error, IssueUpdateRequest>({
mutationKey: ["issues", "update"],
mutationFn: issue.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.all });
},
});
return useMutation<IssueRecord, Error, IssueUpdateRequest>({
mutationKey: ["issues", "update"],
mutationFn: issue.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.all });
},
});
}
export function useDeleteIssue() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<SuccessResponse, Error, number>({
mutationKey: ["issues", "delete"],
mutationFn: issue.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.all });
},
});
return useMutation<SuccessResponse, Error, number>({
mutationKey: ["issues", "delete"],
mutationFn: issue.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.all });
},
});
}
export function useReplaceIssueStatus() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<unknown, Error, IssuesReplaceStatusRequest>({
mutationKey: ["issues", "replace-status"],
mutationFn: issue.replaceStatus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.all });
},
});
return useMutation<unknown, Error, IssuesReplaceStatusRequest>({
mutationKey: ["issues", "replace-status"],
mutationFn: issue.replaceStatus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.all });
},
});
}

View File

@@ -1,113 +1,113 @@
import type {
OrgAddMemberRequest,
OrganisationMemberRecord,
OrganisationMemberResponse,
OrgAddMemberRequest,
OrganisationMemberRecord,
OrganisationMemberResponse,
} from "@sprint/shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query/keys";
import { organisation } from "@/lib/server";
export function useOrganisations() {
return useQuery({
queryKey: queryKeys.organisations.byUser(),
queryFn: organisation.byUser,
});
return useQuery({
queryKey: queryKeys.organisations.byUser(),
queryFn: organisation.byUser,
});
}
export function useOrganisationMembers(organisationId?: number | null) {
return useQuery<OrganisationMemberResponse[]>({
queryKey: queryKeys.organisations.members(organisationId ?? 0),
queryFn: () => organisation.members(organisationId ?? 0),
enabled: Boolean(organisationId),
});
return useQuery<OrganisationMemberResponse[]>({
queryKey: queryKeys.organisations.members(organisationId ?? 0),
queryFn: () => organisation.members(organisationId ?? 0),
enabled: Boolean(organisationId),
});
}
export function useCreateOrganisation() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["organisations", "create"],
mutationFn: organisation.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
return useMutation({
mutationKey: ["organisations", "create"],
mutationFn: organisation.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
}
export function useUpdateOrganisation() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["organisations", "update"],
mutationFn: organisation.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
return useMutation({
mutationKey: ["organisations", "update"],
mutationFn: organisation.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
}
export function useDeleteOrganisation() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["organisations", "delete"],
mutationFn: organisation.remove,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
return useMutation({
mutationKey: ["organisations", "delete"],
mutationFn: organisation.remove,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
}
export function useAddOrganisationMember() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<OrganisationMemberRecord, Error, OrgAddMemberRequest>({
mutationKey: ["organisations", "members", "add"],
mutationFn: organisation.addMember,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.organisations.members(variables.organisationId),
});
},
});
return useMutation<OrganisationMemberRecord, Error, OrgAddMemberRequest>({
mutationKey: ["organisations", "members", "add"],
mutationFn: organisation.addMember,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.organisations.members(variables.organisationId),
});
},
});
}
export function useRemoveOrganisationMember() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["organisations", "members", "remove"],
mutationFn: organisation.removeMember,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.organisations.members(variables.organisationId),
});
},
});
return useMutation({
mutationKey: ["organisations", "members", "remove"],
mutationFn: organisation.removeMember,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.organisations.members(variables.organisationId),
});
},
});
}
export function useUpdateOrganisationMemberRole() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["organisations", "members", "update-role"],
mutationFn: organisation.updateMemberRole,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.organisations.members(variables.organisationId),
});
},
});
return useMutation({
mutationKey: ["organisations", "members", "update-role"],
mutationFn: organisation.updateMemberRole,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.organisations.members(variables.organisationId),
});
},
});
}
export function useUploadOrganisationIcon() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<string, Error, { file: File; organisationId: number }>({
mutationKey: ["organisations", "upload-icon"],
mutationFn: ({ file, organisationId }) => organisation.uploadIcon(file, organisationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
return useMutation<string, Error, { file: File; organisationId: number }>({
mutationKey: ["organisations", "upload-icon"],
mutationFn: ({ file, organisationId }) => organisation.uploadIcon(file, organisationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
},
});
}

View File

@@ -1,55 +1,55 @@
import type {
ProjectCreateRequest,
ProjectRecord,
ProjectResponse,
ProjectUpdateRequest,
ProjectCreateRequest,
ProjectRecord,
ProjectResponse,
ProjectUpdateRequest,
} from "@sprint/shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query/keys";
import { project } from "@/lib/server";
export function useProjects(organisationId?: number | null) {
return useQuery<ProjectResponse[]>({
queryKey: queryKeys.projects.byOrganisation(organisationId ?? 0),
queryFn: () => project.byOrganisation(organisationId ?? 0),
enabled: Boolean(organisationId),
});
return useQuery<ProjectResponse[]>({
queryKey: queryKeys.projects.byOrganisation(organisationId ?? 0),
queryFn: () => project.byOrganisation(organisationId ?? 0),
enabled: Boolean(organisationId),
});
}
export function useCreateProject() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<ProjectRecord, Error, ProjectCreateRequest>({
mutationKey: ["projects", "create"],
mutationFn: project.create,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.projects.byOrganisation(variables.organisationId),
});
},
});
return useMutation<ProjectRecord, Error, ProjectCreateRequest>({
mutationKey: ["projects", "create"],
mutationFn: project.create,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: queryKeys.projects.byOrganisation(variables.organisationId),
});
},
});
}
export function useUpdateProject() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<ProjectRecord, Error, ProjectUpdateRequest>({
mutationKey: ["projects", "update"],
mutationFn: project.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
},
});
return useMutation<ProjectRecord, Error, ProjectUpdateRequest>({
mutationKey: ["projects", "update"],
mutationFn: project.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
},
});
}
export function useDeleteProject() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["projects", "delete"],
mutationFn: project.remove,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
},
});
return useMutation({
mutationKey: ["projects", "delete"],
mutationFn: project.remove,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
},
});
}

View File

@@ -4,45 +4,45 @@ import { queryKeys } from "@/lib/query/keys";
import { sprint } from "@/lib/server";
export function useSprints(projectId?: number | null) {
return useQuery<SprintRecord[]>({
queryKey: queryKeys.sprints.byProject(projectId ?? 0),
queryFn: () => sprint.byProject(projectId ?? 0),
enabled: Boolean(projectId),
});
return useQuery<SprintRecord[]>({
queryKey: queryKeys.sprints.byProject(projectId ?? 0),
queryFn: () => sprint.byProject(projectId ?? 0),
enabled: Boolean(projectId),
});
}
export function useCreateSprint() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<SprintRecord, Error, SprintCreateRequest>({
mutationKey: ["sprints", "create"],
mutationFn: sprint.create,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.byProject(variables.projectId) });
},
});
return useMutation<SprintRecord, Error, SprintCreateRequest>({
mutationKey: ["sprints", "create"],
mutationFn: sprint.create,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.byProject(variables.projectId) });
},
});
}
export function useUpdateSprint() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<SprintRecord, Error, SprintUpdateRequest>({
mutationKey: ["sprints", "update"],
mutationFn: sprint.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.all });
},
});
return useMutation<SprintRecord, Error, SprintUpdateRequest>({
mutationKey: ["sprints", "update"],
mutationFn: sprint.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.all });
},
});
}
export function useDeleteSprint() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["sprints", "delete"],
mutationFn: sprint.remove,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.all });
},
});
return useMutation({
mutationKey: ["sprints", "delete"],
mutationFn: sprint.remove,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.all });
},
});
}

View File

@@ -4,47 +4,47 @@ import { queryKeys } from "@/lib/query/keys";
import { timer } from "@/lib/server";
export function useTimerState(issueId?: number | null, options?: { refetchInterval?: number }) {
return useQuery<TimerState>({
queryKey: queryKeys.timers.active(issueId ?? 0),
queryFn: () => timer.get(issueId ?? 0),
enabled: Boolean(issueId),
refetchInterval: options?.refetchInterval,
refetchIntervalInBackground: false,
});
return useQuery<TimerState>({
queryKey: queryKeys.timers.active(issueId ?? 0),
queryFn: () => timer.get(issueId ?? 0),
enabled: Boolean(issueId),
refetchInterval: options?.refetchInterval,
refetchIntervalInBackground: false,
});
}
export function useInactiveTimers(issueId?: number | null, options?: { refetchInterval?: number }) {
return useQuery<TimerState[]>({
queryKey: queryKeys.timers.inactive(issueId ?? 0),
queryFn: () => timer.getInactive(issueId ?? 0),
enabled: Boolean(issueId),
refetchInterval: options?.refetchInterval,
refetchIntervalInBackground: false,
});
return useQuery<TimerState[]>({
queryKey: queryKeys.timers.inactive(issueId ?? 0),
queryFn: () => timer.getInactive(issueId ?? 0),
enabled: Boolean(issueId),
refetchInterval: options?.refetchInterval,
refetchIntervalInBackground: false,
});
}
export function useToggleTimer() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<TimerState, Error, TimerToggleRequest>({
mutationKey: ["timers", "toggle"],
mutationFn: timer.toggle,
onSuccess: (data, variables) => {
queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data);
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
},
});
return useMutation<TimerState, Error, TimerToggleRequest>({
mutationKey: ["timers", "toggle"],
mutationFn: timer.toggle,
onSuccess: (data, variables) => {
queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data);
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
},
});
}
export function useEndTimer() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<TimerState, Error, TimerEndRequest>({
mutationKey: ["timers", "end"],
mutationFn: timer.end,
onSuccess: (data, variables) => {
queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data);
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
},
});
return useMutation<TimerState, Error, TimerEndRequest>({
mutationKey: ["timers", "end"],
mutationFn: timer.end,
onSuccess: (data, variables) => {
queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data);
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
},
});
}

View File

@@ -4,33 +4,33 @@ import { queryKeys } from "@/lib/query/keys";
import { user } from "@/lib/server";
export function useUserByUsername(username?: string | null) {
return useQuery<UserRecord>({
queryKey: queryKeys.users.byUsername(username ?? ""),
queryFn: () => user.byUsername(username ?? ""),
enabled: Boolean(username),
});
return useQuery<UserRecord>({
queryKey: queryKeys.users.byUsername(username ?? ""),
queryFn: () => user.byUsername(username ?? ""),
enabled: Boolean(username),
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<UserRecord, Error, UserUpdateRequest>({
mutationKey: ["users", "update"],
mutationFn: user.update,
onSuccess: (_data) => {
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
},
});
return useMutation<UserRecord, Error, UserUpdateRequest>({
mutationKey: ["users", "update"],
mutationFn: user.update,
onSuccess: (_data) => {
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
},
});
}
export function useUploadAvatar() {
const queryClient = useQueryClient();
const queryClient = useQueryClient();
return useMutation<string, Error, File>({
mutationKey: ["users", "upload-avatar"],
mutationFn: user.uploadAvatar,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
},
});
return useMutation<string, Error, File>({
mutationKey: ["users", "upload-avatar"],
mutationFn: user.uploadAvatar,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
},
});
}

View File

@@ -1,33 +1,33 @@
// query key factory for granular cache invalidation
export const queryKeys = {
organisations: {
all: ["organisations"] as const,
byUser: () => [...queryKeys.organisations.all, "by-user"] as const,
members: (orgId: number) => [...queryKeys.organisations.all, orgId, "members"] as const,
},
projects: {
all: ["projects"] as const,
byOrganisation: (orgId: number) => [...queryKeys.projects.all, "by-org", orgId] as const,
},
issues: {
all: ["issues"] as const,
byProject: (projectId: number) => [...queryKeys.issues.all, "by-project", projectId] as const,
statusCount: (organisationId: number, status: string) =>
[...queryKeys.issues.all, "status-count", organisationId, status] as const,
},
sprints: {
all: ["sprints"] as const,
byProject: (projectId: number) => [...queryKeys.sprints.all, "by-project", projectId] as const,
},
timers: {
all: ["timers"] as const,
active: (issueId: number) => [...queryKeys.timers.all, "active", issueId] as const,
inactive: (issueId: number) => [...queryKeys.timers.all, "inactive", issueId] as const,
list: (issueId: number) => [...queryKeys.timers.all, "list", issueId] as const,
},
users: {
all: ["users"] as const,
byUsername: (username: string) => [...queryKeys.users.all, "by-username", username] as const,
},
organisations: {
all: ["organisations"] as const,
byUser: () => [...queryKeys.organisations.all, "by-user"] as const,
members: (orgId: number) => [...queryKeys.organisations.all, orgId, "members"] as const,
},
projects: {
all: ["projects"] as const,
byOrganisation: (orgId: number) => [...queryKeys.projects.all, "by-org", orgId] as const,
},
issues: {
all: ["issues"] as const,
byProject: (projectId: number) => [...queryKeys.issues.all, "by-project", projectId] as const,
statusCount: (organisationId: number, status: string) =>
[...queryKeys.issues.all, "status-count", organisationId, status] as const,
},
sprints: {
all: ["sprints"] as const,
byProject: (projectId: number) => [...queryKeys.sprints.all, "by-project", projectId] as const,
},
timers: {
all: ["timers"] as const,
active: (issueId: number) => [...queryKeys.timers.all, "active", issueId] as const,
inactive: (issueId: number) => [...queryKeys.timers.all, "inactive", issueId] as const,
list: (issueId: number) => [...queryKeys.timers.all, "list", issueId] as const,
},
users: {
all: ["users"] as const,
byUsername: (username: string) => [...queryKeys.users.all, "by-username", username] as const,
},
};

View File

@@ -8,28 +8,28 @@ export * as timer from "@/lib/server/timer";
export * as user from "@/lib/server/user";
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;
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 && 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;
}
if ("error" in error && typeof error.error === "string") {
return error.error || fallback;
}
return fallback;
}
return fallback;
}
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(", ");
}
return error.error;
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(", ");
}
return error.error;
}

View File

@@ -3,17 +3,17 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function byProject(projectId: number): Promise<IssueResponse[]> {
const url = new URL(`${getServerURL()}/issues/by-project`);
url.searchParams.set("projectId", `${projectId}`);
const url = new URL(`${getServerURL()}/issues/by-project`);
url.searchParams.set("projectId", `${projectId}`);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get issues by project (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get issues by project (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,26 +3,26 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function create(request: IssueCreateRequest): Promise<IssueRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issue/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/issue/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to create issue (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
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) {
throw new Error(`failed to create issue (${res.status})`);
}
return data;
const data = (await res.json()) as IssueRecord;
if (!data.id) {
throw new Error(`failed to create issue (${res.status})`);
}
return data;
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function remove(issueId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
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",
});
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 message = await getErrorMessage(res, `failed to delete issue (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete issue (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function replaceStatus(request: IssuesReplaceStatusRequest): Promise<ReplaceStatusResponse> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issues/replace-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/issues/replace-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to replace status (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to replace status (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,18 +3,18 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
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);
const url = new URL(`${getServerURL()}/issues/status-count`);
url.searchParams.set("organisationId", `${organisationId}`);
url.searchParams.set("status", status);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get issue status count (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get issue status count (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function update(input: IssueUpdateRequest): Promise<IssueRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/issue/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(input),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/issue/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(input),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update issue (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update issue (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function addMember(request: OrgAddMemberRequest): Promise<OrganisationMemberRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/add-member`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/add-member`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to add member (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to add member (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,14 +3,14 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function byUser(): Promise<OrganisationResponse[]> {
const res = await fetch(`${getServerURL()}/organisations/by-user`, {
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisations/by-user`, {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get organisations (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get organisations (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,26 +3,26 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function create(request: OrgCreateRequest): Promise<OrganisationRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to create organisation (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
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) {
throw new Error(`failed to create organisation (${res.status})`);
}
return data;
const data = (await res.json()) as OrganisationRecord;
if (!data.id) {
throw new Error(`failed to create organisation (${res.status})`);
}
return data;
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function remove(organisationId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: organisationId }),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: organisationId }),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete organisation (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete organisation (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,17 +3,17 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function members(organisationId: number): Promise<OrganisationMemberResponse[]> {
const url = new URL(`${getServerURL()}/organisation/members`);
url.searchParams.set("organisationId", `${organisationId}`);
const url = new URL(`${getServerURL()}/organisation/members`);
url.searchParams.set("organisationId", `${organisationId}`);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get members (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get members (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function removeMember(request: OrgRemoveMemberRequest): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/remove-member`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/remove-member`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to remove member (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to remove member (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function update(input: OrgUpdateRequest): Promise<OrganisationRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(input),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(input),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update organisation (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update organisation (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,24 +3,24 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function updateMemberRole(
request: OrgUpdateMemberRoleRequest,
request: OrgUpdateMemberRoleRequest,
): Promise<OrganisationMemberRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/organisation/update-member-role`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/update-member-role`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update member role (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update member role (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -2,41 +2,41 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function uploadIcon(file: File, organisationId: number): Promise<string> {
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
if (file.size > MAX_FILE_SIZE) {
throw new Error("File size exceeds 5MB limit");
}
if (file.size > MAX_FILE_SIZE) {
throw new Error("File size exceeds 5MB limit");
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type. Allowed types: png, jpg, jpeg, webp, gif");
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type. Allowed types: png, jpg, jpeg, webp, gif");
}
const formData = new FormData();
formData.append("file", file);
formData.append("organisationId", organisationId.toString());
const formData = new FormData();
formData.append("file", file);
formData.append("organisationId", organisationId.toString());
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(`${getServerURL()}/organisation/upload-icon`, {
method: "POST",
headers,
body: formData,
credentials: "include",
});
const res = await fetch(`${getServerURL()}/organisation/upload-icon`, {
method: "POST",
headers,
body: formData,
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `Failed to upload icon (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `Failed to upload icon (${res.status})`);
throw new Error(message);
}
const data = await res.json();
if (data.iconURL) {
return data.iconURL;
}
const data = await res.json();
if (data.iconURL) {
return data.iconURL;
}
throw new Error("Failed to upload icon");
throw new Error("Failed to upload icon");
}

View File

@@ -3,17 +3,17 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function byOrganisation(organisationId: number): Promise<ProjectResponse[]> {
const url = new URL(`${getServerURL()}/projects/by-organisation`);
url.searchParams.set("organisationId", `${organisationId}`);
const url = new URL(`${getServerURL()}/projects/by-organisation`);
url.searchParams.set("organisationId", `${organisationId}`);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get projects by organisation (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get projects by organisation (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,26 +3,26 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function create(request: ProjectCreateRequest): Promise<ProjectRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/project/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/project/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to create project (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
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) {
throw new Error(`failed to create project (${res.status})`);
}
return data;
const data = (await res.json()) as ProjectRecord;
if (!data.id) {
throw new Error(`failed to create project (${res.status})`);
}
return data;
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function remove(projectId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/project/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: projectId }),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/project/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: projectId }),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete project (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete project (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function update(input: ProjectUpdateRequest): Promise<ProjectRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/project/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(input),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/project/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(input),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update project (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update project (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,17 +3,17 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function byProject(projectId: number): Promise<SprintRecord[]> {
const url = new URL(`${getServerURL()}/sprints/by-project`);
url.searchParams.set("projectId", `${projectId}`);
const url = new URL(`${getServerURL()}/sprints/by-project`);
url.searchParams.set("projectId", `${projectId}`);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get sprints (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get sprints (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,29 +3,29 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function create(input: SprintCreateRequest): Promise<SprintRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/sprint/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
...input,
name: input.name.trim(),
}),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/sprint/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
...input,
name: input.name.trim(),
}),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to create sprint (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
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) {
throw new Error(`failed to create sprint (${res.status})`);
}
return data;
const data = (await res.json()) as SprintRecord;
if (!data.id) {
throw new Error(`failed to create sprint (${res.status})`);
}
return data;
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function remove(sprintId: number): Promise<SuccessResponse> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/sprint/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: sprintId }),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/sprint/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({ id: sprintId }),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete sprint (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to delete sprint (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,25 +3,25 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function update(input: SprintUpdateRequest): Promise<SprintRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/sprint/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
...input,
name: input.name?.trim(),
}),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/sprint/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify({
...input,
name: input.name?.trim(),
}),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update sprint (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update sprint (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function end(request: TimerEndRequest): Promise<TimerState> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/timer/end`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/timer/end`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to end timer (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to end timer (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,17 +3,17 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function get(issueId: number): Promise<TimerState> {
const url = new URL(`${getServerURL()}/timer/get`);
url.searchParams.set("issueId", `${issueId}`);
const url = new URL(`${getServerURL()}/timer/get`);
url.searchParams.set("issueId", `${issueId}`);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get timer (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get timer (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,18 +3,18 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function getInactive(issueId: number): Promise<TimerState[]> {
const url = new URL(`${getServerURL()}/timer/get-inactive`);
url.searchParams.set("issueId", `${issueId}`);
const url = new URL(`${getServerURL()}/timer/get-inactive`);
url.searchParams.set("issueId", `${issueId}`);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get timers (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get timers (${res.status})`);
throw new Error(message);
}
const data = (await res.json()) as TimerState[];
return data ?? [];
const data = (await res.json()) as TimerState[];
return data ?? [];
}

View File

@@ -2,28 +2,28 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
type TimerListInput = {
limit?: number;
offset?: number;
limit?: number;
offset?: number;
};
export async function list(input: TimerListInput = {}): Promise<unknown> {
const url = new URL(`${getServerURL()}/timers`);
if (input.limit != null) url.searchParams.set("limit", `${input.limit}`);
if (input.offset != null) url.searchParams.set("offset", `${input.offset}`);
const url = new URL(`${getServerURL()}/timers`);
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 = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(url.toString(), {
headers,
credentials: "include",
});
const res = await fetch(url.toString(), {
headers,
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get timers (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get timers (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,22 +3,22 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function toggle(request: TimerToggleRequest): Promise<TimerState> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/timer/toggle`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/timer/toggle`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to toggle timer (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to toggle timer (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,17 +3,17 @@ import { getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function byUsername(username: string): Promise<UserRecord> {
const url = new URL(`${getServerURL()}/user/by-username`);
url.searchParams.set("username", username);
const url = new URL(`${getServerURL()}/user/by-username`);
url.searchParams.set("username", username);
const res = await fetch(url.toString(), {
credentials: "include",
});
const res = await fetch(url.toString(), {
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get user (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `failed to get user (${res.status})`);
throw new Error(message);
}
return res.json();
return res.json();
}

View File

@@ -3,26 +3,26 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
export async function update(request: UserUpdateRequest): Promise<UserRecord> {
const csrfToken = getCsrfToken();
const csrfToken = getCsrfToken();
const res = await fetch(`${getServerURL()}/user/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
const res = await fetch(`${getServerURL()}/user/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
},
body: JSON.stringify(request),
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `failed to update user (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
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) {
throw new Error(`failed to update user (${res.status})`);
}
return data;
const data = (await res.json()) as UserRecord;
if (!data.id) {
throw new Error(`failed to update user (${res.status})`);
}
return data;
}

View File

@@ -2,40 +2,40 @@ import { getCsrfToken, getServerURL } from "@/lib/utils";
import { getErrorMessage } from "..";
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"];
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
if (file.size > MAX_FILE_SIZE) {
throw new Error("File size exceeds 5MB limit");
}
if (file.size > MAX_FILE_SIZE) {
throw new Error("File size exceeds 5MB limit");
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type. Allowed types: png, jpg, jpeg, webp, gif");
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error("Invalid file type. Allowed types: png, jpg, jpeg, webp, gif");
}
const formData = new FormData();
formData.append("file", file);
const formData = new FormData();
formData.append("file", file);
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const csrfToken = getCsrfToken();
const headers: HeadersInit = {};
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
const res = await fetch(`${getServerURL()}/user/upload-avatar`, {
method: "POST",
headers,
body: formData,
credentials: "include",
});
const res = await fetch(`${getServerURL()}/user/upload-avatar`, {
method: "POST",
headers,
body: formData,
credentials: "include",
});
if (!res.ok) {
const message = await getErrorMessage(res, `Failed to upload avatar (${res.status})`);
throw new Error(message);
}
if (!res.ok) {
const message = await getErrorMessage(res, `Failed to upload avatar (${res.status})`);
throw new Error(message);
}
const data = await res.json();
if (data.avatarURL) {
return data.avatarURL;
}
const data = await res.json();
if (data.avatarURL) {
return data.avatarURL;
}
throw new Error("Failed to upload avatar");
throw new Error("Failed to upload avatar");
}

View File

@@ -2,63 +2,63 @@ import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}
export function issueID(key: string, num: number) {
return `${key}-${num.toString().padStart(3, "0")}`;
return `${key}-${num.toString().padStart(3, "0")}`;
}
export function getCsrfToken(): string | null {
return sessionStorage.getItem("csrfToken");
return sessionStorage.getItem("csrfToken");
}
export function setCsrfToken(token: string): void {
sessionStorage.setItem("csrfToken", token);
sessionStorage.setItem("csrfToken", token);
}
export function clearAuth(): void {
sessionStorage.removeItem("csrfToken");
localStorage.removeItem("user");
localStorage.removeItem("selectedOrganisationId");
localStorage.removeItem("selectedProjectId");
sessionStorage.removeItem("csrfToken");
localStorage.removeItem("user");
localStorage.removeItem("selectedOrganisationId");
localStorage.removeItem("selectedProjectId");
}
export function capitalise(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
return str.charAt(0).toUpperCase() + str.slice(1);
}
const ENV_SERVER_URL = import.meta.env.VITE_SERVER_URL?.trim();
export function getServerURL() {
let serverURL =
localStorage.getItem("serverURL") || // user-defined server URL
ENV_SERVER_URL || // environment variable
"https://tnirps.ob248.com"; // fallback
if (serverURL.endsWith("/")) {
serverURL = serverURL.slice(0, -1);
}
return serverURL;
let serverURL =
localStorage.getItem("serverURL") || // user-defined server URL
ENV_SERVER_URL || // environment variable
"https://tnirps.ob248.com"; // fallback
if (serverURL.endsWith("/")) {
serverURL = serverURL.slice(0, -1);
}
return serverURL;
}
export function formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}`;
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}`;
}
export const DARK_TEXT_COLOUR = "#0a0a0a";
const THRESHOLD = 0.6;
export const isLight = (hex: string): boolean => {
const num = Number.parseInt(hex.replace("#", ""), 16);
const r = (num >> 16) & 255;
const g = (num >> 8) & 255;
const b = num & 255;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > THRESHOLD;
const num = Number.parseInt(hex.replace("#", ""), 16);
const r = (num >> 16) & 255;
const g = (num >> 8) & 255;
const b = num & 255;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > THRESHOLD;
};