diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 2e3c9f0..d4a8a4a 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -20,12 +20,12 @@ const main = async () => { "/issue/create": withCors(withAuth(routes.issueCreate)), "/issue/update": withCors(withAuth(routes.issueUpdate)), "/issue/delete": withCors(withAuth(routes.issueDelete)), - "/issues/:projectKey": withCors(withAuth(routes.issuesInProject)), + "/issues/by-project": withCors(withAuth(routes.issuesByProject)), "/issues/all": withCors(withAuth(routes.issues)), "/organisation/create": withCors(withAuth(routes.organisationCreate)), "/organisation/by-id": withCors(withAuth(routes.organisationById)), - "/organisation/by-user": withCors(withAuth(routes.organisationByUser)), + "/organisations/by-user": withCors(withAuth(routes.organisationByUser)), "/organisation/update": withCors(withAuth(routes.organisationUpdate)), "/organisation/delete": withCors(withAuth(routes.organisationDelete)), "/organisation/add-member": withCors(withAuth(routes.organisationAddMember)), diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 6c7d4ef..ae57508 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -4,8 +4,8 @@ import authRegister from "./auth/register"; import issueCreate from "./issue/create"; import issueDelete from "./issue/delete"; import issueUpdate from "./issue/update"; -import issuesInProject from "./issues/[projectKey]"; import issues from "./issues/all"; +import issuesByProject from "./issues/by-project"; import organisationAddMember from "./organisation/add-member"; import organisationById from "./organisation/by-id"; import organisationByUser from "./organisation/by-user"; @@ -29,7 +29,7 @@ export const routes = { issueDelete, issueUpdate, - issuesInProject, + issuesByProject, issues, organisationCreate, diff --git a/packages/backend/src/routes/issues/[projectKey].ts b/packages/backend/src/routes/issues/[projectKey].ts deleted file mode 100644 index 90bcc6a..0000000 --- a/packages/backend/src/routes/issues/[projectKey].ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { BunRequest } from "bun"; -import { getIssuesWithAssigneeByProject, getProjectByKey } from "../../db/queries"; - -export default async function issuesInProject(req: BunRequest<"/issues/:projectKey">) { - const { projectKey } = req.params; - - const project = await getProjectByKey(projectKey); - if (!project) { - return new Response(`project not found: provided ${projectKey}`, { status: 404 }); - } - const issues = await getIssuesWithAssigneeByProject(project.id); - - return Response.json(issues); -} diff --git a/packages/backend/src/routes/issues/by-project.ts b/packages/backend/src/routes/issues/by-project.ts new file mode 100644 index 0000000..e4b0ab1 --- /dev/null +++ b/packages/backend/src/routes/issues/by-project.ts @@ -0,0 +1,15 @@ +import type { AuthedRequest } from "../../auth/middleware"; +import { getIssuesWithAssigneeByProject, getProjectByID } from "../../db/queries"; + +export default async function issuesByProject(req: AuthedRequest) { + const url = new URL(req.url); + const projectId = url.searchParams.get("projectId"); + + const project = await getProjectByID(Number(projectId)); + if (!project) { + return new Response(`project not found: provided ${projectId}`, { status: 404 }); + } + const issues = await getIssuesWithAssigneeByProject(project.id); + + return Response.json(issues); +} diff --git a/packages/frontend/src/Index.tsx b/packages/frontend/src/Index.tsx index eee38b9..1341b9d 100644 --- a/packages/frontend/src/Index.tsx +++ b/packages/frontend/src/Index.tsx @@ -17,8 +17,9 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { ResizablePanel, ResizablePanelGroup, ResizableSeparator } from "@/components/ui/resizable"; +import { issue, organisation, project } from "@/lib/server"; import { getAuthHeaders, getServerURL } from "@/lib/utils"; -import { ResizablePanel, ResizablePanelGroup, ResizableSeparator } from "./components/ui/resizable"; function Index() { const user = JSON.parse(localStorage.getItem("user") || "{}") as UserRecord; @@ -35,27 +36,35 @@ function Index() { const refetchOrganisations = async (options?: { selectOrganisationId?: number }) => { try { - const res = await fetch(`${getServerURL()}/organisation/by-user?userId=${user.id}`, { - headers: getAuthHeaders(), - }); - const data = (await res.json()) as Array; + await organisation.byUser({ + userId: user.id, + onSuccess: (data) => { + const organisations = data as OrganisationResponse[]; + setOrganisations(organisations); - setOrganisations(data); + // select newly created organisation + if (options?.selectOrganisationId) { + const created = organisations.find( + (o) => o.Organisation.id === options.selectOrganisationId, + ); + if (created) { + setSelectedOrganisation(created); + return; + } + } - // select newly created organisation - if (options?.selectOrganisationId) { - const created = data.find((o) => o.Organisation.id === options.selectOrganisationId); - if (created) { - setSelectedOrganisation(created); - return; - } - } - - // preserve previously selected organisation - setSelectedOrganisation((prev) => { - if (!prev) return data[0] || null; - const stillExists = data.find((o) => o.Organisation.id === prev.Organisation.id); - return stillExists || data[0] || null; + // preserve previously selected organisation + setSelectedOrganisation((prev) => { + if (!prev) return organisations[0] || null; + const stillExists = organisations.find( + (o) => o.Organisation.id === prev.Organisation.id, + ); + return stillExists || organisations[0] || null; + }); + }, + onError: (error) => { + console.error("error fetching organisations:", error); + }, }); } catch (err) { console.error("error fetching organisations:", err); @@ -74,30 +83,31 @@ function Index() { const refetchProjects = async (organisationId: number, options?: { selectProjectId?: number }) => { try { - const res = await fetch( - `${getServerURL()}/projects/by-organisation?organisationId=${organisationId}`, - { - headers: getAuthHeaders(), + await project.byOrganisation({ + organisationId, + onSuccess: (data) => { + const projects = data as ProjectResponse[]; + setProjects(projects); + + // select newly created project + if (options?.selectProjectId) { + const created = projects.find((p) => p.Project.id === options.selectProjectId); + if (created) { + setSelectedProject(created); + return; + } + } + + // preserve previously selected project + setSelectedProject((prev) => { + if (!prev) return projects[0] || null; + const stillExists = projects.find((p) => p.Project.id === prev.Project.id); + return stillExists || projects[0] || null; + }); + }, + onError: (error) => { + console.error("error fetching projects:", error); }, - ); - - const data = (await res.json()) as ProjectResponse[]; - setProjects(data); - - // select newly created project - if (options?.selectProjectId) { - const created = data.find((p) => p.Project.id === options.selectProjectId); - if (created) { - setSelectedProject(created); - return; - } - } - - // preserve previously selected project - setSelectedProject((prev) => { - if (!prev) return data[0] || null; - const stillExists = data.find((p) => p.Project.id === prev.Project.id); - return stillExists || data[0] || null; }); } catch (err) { console.error("error fetching projects:", err); @@ -124,11 +134,17 @@ function Index() { const refetchIssues = async (projectKey: string) => { try { - const res = await fetch(`${getServerURL()}/issues/${projectKey}`, { - headers: getAuthHeaders(), + await issue.byProject({ + projectId: selectedProject?.Project.id || 0, + onSuccess: (data) => { + const issues = data as IssueResponse[]; + setIssues(issues); + }, + onError: (error) => { + console.error("error fetching issues:", error); + setIssues([]); + }, }); - const data = (await res.json()) as IssueResponse[]; - setIssues(data); } catch (err) { console.error("error fetching issues:", err); setIssues([]); diff --git a/packages/frontend/src/Organisations.tsx b/packages/frontend/src/Organisations.tsx index 20f5d0a..ec27d95 100644 --- a/packages/frontend/src/Organisations.tsx +++ b/packages/frontend/src/Organisations.tsx @@ -2,6 +2,7 @@ import type { OrganisationResponse, UserRecord } from "@issue/shared"; import { useCallback, useEffect, useState } from "react"; import { OrganisationSelect } from "@/components/organisation-select"; import { SettingsPageLayout } from "@/components/settings-page-layout"; +import { organisation } from "@/lib/server"; import { getAuthHeaders, getServerURL } from "@/lib/utils"; function Organisations() { @@ -13,32 +14,35 @@ function Organisations() { const refetchOrganisations = useCallback( async (options?: { selectOrganisationId?: number }) => { try { - const res = await fetch(`${getServerURL()}/organisation/by-user?userId=${user.id}`, { - headers: getAuthHeaders(), - }); + await organisation.byUser({ + userId: user.id, + onSuccess: (data) => { + const organisations = data as OrganisationResponse[]; + setOrganisations(organisations); - if (!res.ok) { - console.error(await res.text()); - setOrganisations([]); - setSelectedOrganisation(null); - return; - } + if (options?.selectOrganisationId) { + const created = organisations.find( + (o) => o.Organisation.id === options.selectOrganisationId, + ); + if (created) { + setSelectedOrganisation(created); + return; + } + } - const data = (await res.json()) as Array; - setOrganisations(data); - - if (options?.selectOrganisationId) { - const created = data.find((o) => o.Organisation.id === options.selectOrganisationId); - if (created) { - setSelectedOrganisation(created); - return; - } - } - - setSelectedOrganisation((prev) => { - if (!prev) return data[0] || null; - const stillExists = data.find((o) => o.Organisation.id === prev.Organisation.id); - return stillExists || data[0] || null; + setSelectedOrganisation((prev) => { + if (!prev) return organisations[0] || null; + const stillExists = organisations.find( + (o) => o.Organisation.id === prev.Organisation.id, + ); + return stillExists || organisations[0] || null; + }); + }, + onError: (error) => { + console.error(error); + setOrganisations([]); + setSelectedOrganisation(null); + }, }); } catch (err) { console.error("error fetching organisations:", err); diff --git a/packages/frontend/src/components/create-issue.tsx b/packages/frontend/src/components/create-issue.tsx index 94c2e2d..7f3ff6b 100644 --- a/packages/frontend/src/components/create-issue.tsx +++ b/packages/frontend/src/components/create-issue.tsx @@ -10,7 +10,8 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { cn, getAuthHeaders, getServerURL } from "@/lib/utils"; +import { issue } from "@/lib/server"; +import { cn } from "@/lib/utils"; export function CreateIssue({ projectId, @@ -88,36 +89,24 @@ export function CreateIssue({ setSubmitting(true); try { - const url = new URL(`${getServerURL()}/issue/create`); - url.searchParams.set("projectId", `${projectId}`); - url.searchParams.set("title", title.trim()); - url.searchParams.set("description", description.trim()); - - const res = await fetch(url.toString(), { - headers: getAuthHeaders(), + await issue.create({ + projectId, + title, + description, + onSuccess: async (data) => { + setOpen(false); + reset(); + try { + await completeAction?.(data.id); + } catch (actionErr) { + console.error(actionErr); + } + }, + onError: (message) => { + setError(message); + setSubmitting(false); + }, }); - - if (!res.ok) { - const message = await res.text(); - setError(message || `failed to create issue (${res.status})`); - setSubmitting(false); - return; - } - - const issue = (await res.json()) as { id?: number }; - if (!issue.id) { - setError("failed to create issue"); - setSubmitting(false); - return; - } - - setOpen(false); - reset(); - try { - await completeAction?.(issue.id); - } catch (actionErr) { - console.error(actionErr); - } } catch (err) { console.error(err); setError("failed to create issue"); diff --git a/packages/frontend/src/components/create-organisation.tsx b/packages/frontend/src/components/create-organisation.tsx index 731660b..eda3e36 100644 --- a/packages/frontend/src/components/create-organisation.tsx +++ b/packages/frontend/src/components/create-organisation.tsx @@ -10,7 +10,8 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { cn, getAuthHeaders, getServerURL } from "@/lib/utils"; +import { organisation } from "@/lib/server"; +import { cn } from "@/lib/utils"; const slugify = (value: string) => value @@ -88,39 +89,25 @@ export function CreateOrganisation({ setSubmitting(true); try { - 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()); - } - - const res = await fetch(url.toString(), { - headers: getAuthHeaders(), + await organisation.create({ + name, + slug, + description, + userId, + onSuccess: async (data) => { + setOpen(false); + reset(); + try { + await completeAction?.(data.id); + } catch (actionErr) { + console.error(actionErr); + } + }, + onError: (err) => { + setError(err || "failed to create organisation"); + setSubmitting(false); + }, }); - - if (!res.ok) { - const message = await res.text(); - setError(message || `failed to create organisation (${res.status})`); - setSubmitting(false); - return; - } - - const organisation = (await res.json()) as { id?: number }; - if (!organisation.id) { - setError("failed to create organisation"); - setSubmitting(false); - return; - } - - setOpen(false); - reset(); - try { - await completeAction?.(organisation.id); - } catch (actionErr) { - console.error(actionErr); - } } catch (err) { console.error(err); setError("failed to create organisation"); diff --git a/packages/frontend/src/components/create-project.tsx b/packages/frontend/src/components/create-project.tsx index 65ff8d8..f3248c6 100644 --- a/packages/frontend/src/components/create-project.tsx +++ b/packages/frontend/src/components/create-project.tsx @@ -1,3 +1,4 @@ +import type { ProjectRecord } from "@issue/shared"; import { type FormEvent, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { @@ -10,7 +11,8 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { cn, getAuthHeaders, getServerURL } from "@/lib/utils"; +import { project } from "@/lib/server"; +import { cn } from "@/lib/utils"; const keyify = (value: string) => value @@ -94,37 +96,27 @@ export function CreateProject({ setSubmitting(true); try { - const url = new URL(`${getServerURL()}/project/create`); - url.searchParams.set("key", key); - url.searchParams.set("name", name.trim()); - url.searchParams.set("creatorId", `${userId}`); - url.searchParams.set("organisationId", `${organisationId}`); + await project.create({ + key, + name, + creatorId: userId, + organisationId, + onSuccess: async (data) => { + const project = data as ProjectRecord; - const res = await fetch(url.toString(), { - headers: getAuthHeaders(), + setOpen(false); + reset(); + try { + await completeAction?.(project.id); + } catch (actionErr) { + console.error(actionErr); + } + }, + onError: (message) => { + setError(message); + setSubmitting(false); + }, }); - - if (!res.ok) { - const message = await res.text(); - setError(message || `failed to create project (${res.status})`); - setSubmitting(false); - return; - } - - const project = (await res.json()) as { id?: number }; - if (!project.id) { - setError("failed to create project"); - setSubmitting(false); - return; - } - - setOpen(false); - reset(); - try { - await completeAction?.(project.id); - } catch (actionErr) { - console.error(actionErr); - } } catch (err) { console.error(err); setError("failed to create project"); diff --git a/packages/frontend/src/lib/server/index.ts b/packages/frontend/src/lib/server/index.ts new file mode 100644 index 0000000..cf91a68 --- /dev/null +++ b/packages/frontend/src/lib/server/index.ts @@ -0,0 +1,8 @@ +export * as issue from "./issue"; +export * as organisation from "./organisation"; +export * as project from "./project"; + +export type ServerQueryInput = { + onSuccess?: (data: any, res: Response) => void; + onError?: (error: any) => void; +}; diff --git a/packages/frontend/src/lib/server/issue/byProject.ts b/packages/frontend/src/lib/server/issue/byProject.ts new file mode 100644 index 0000000..51ca384 --- /dev/null +++ b/packages/frontend/src/lib/server/issue/byProject.ts @@ -0,0 +1,26 @@ +import { getAuthHeaders, getServerURL } from "@/lib/utils"; +import type { ServerQueryInput } from ".."; + +export async function byProject({ + projectId, + onSuccess, + onError, +}: { + projectId: number; +} & ServerQueryInput) { + const url = new URL(`${getServerURL()}/issues/by-project`); + url.searchParams.set("projectId", `${projectId}`); + + const res = await fetch(url.toString(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to get issues by project (${res.status})`); + } else { + const data = await res.json(); + + onSuccess?.(data, res); + } +} diff --git a/packages/frontend/src/lib/server/issue/create.ts b/packages/frontend/src/lib/server/issue/create.ts new file mode 100644 index 0000000..de3ebe4 --- /dev/null +++ b/packages/frontend/src/lib/server/issue/create.ts @@ -0,0 +1,36 @@ +import { getAuthHeaders, getServerURL } from "@/lib/utils"; +import type { ServerQueryInput } from ".."; + +export async function create({ + projectId, + title, + description, + onSuccess, + onError, +}: { + projectId: number; + title: string; + description: 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()); + + const res = await fetch(url.toString(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to create issue (${res.status})`); + } else { + const data = await res.json(); + if (!data.id) { + onError?.(`failed to create issue (${res.status})`); + return; + } + + onSuccess?.(data, res); + } +} diff --git a/packages/frontend/src/lib/server/issue/index.ts b/packages/frontend/src/lib/server/issue/index.ts new file mode 100644 index 0000000..8d8279e --- /dev/null +++ b/packages/frontend/src/lib/server/issue/index.ts @@ -0,0 +1,2 @@ +export { byProject } from "./byProject"; +export { create } from "./create"; diff --git a/packages/frontend/src/lib/server/organisation/byUser.ts b/packages/frontend/src/lib/server/organisation/byUser.ts new file mode 100644 index 0000000..dedb31e --- /dev/null +++ b/packages/frontend/src/lib/server/organisation/byUser.ts @@ -0,0 +1,30 @@ +import { getAuthHeaders, 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(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to create organisation (${res.status})`); + } else { + const data = await res.json(); + if (!data.id) { + onError?.(`failed to create organisation (${res.status})`); + return; + } + + onSuccess?.(data, res); + } +} diff --git a/packages/frontend/src/lib/server/organisation/create.ts b/packages/frontend/src/lib/server/organisation/create.ts new file mode 100644 index 0000000..8c7656f --- /dev/null +++ b/packages/frontend/src/lib/server/organisation/create.ts @@ -0,0 +1,39 @@ +import { getAuthHeaders, 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()); + + const res = await fetch(url.toString(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to create organisation (${res.status})`); + } else { + const data = await res.json(); + if (!data.id) { + onError?.(`failed to create organisation (${res.status})`); + return; + } + + onSuccess?.(data, res); + } +} diff --git a/packages/frontend/src/lib/server/organisation/index.ts b/packages/frontend/src/lib/server/organisation/index.ts new file mode 100644 index 0000000..1505ba3 --- /dev/null +++ b/packages/frontend/src/lib/server/organisation/index.ts @@ -0,0 +1,2 @@ +export { byUser } from "./byUser"; +export { create } from "./create"; diff --git a/packages/frontend/src/lib/server/project/byOrganisation.ts b/packages/frontend/src/lib/server/project/byOrganisation.ts new file mode 100644 index 0000000..2c58c81 --- /dev/null +++ b/packages/frontend/src/lib/server/project/byOrganisation.ts @@ -0,0 +1,26 @@ +import { getAuthHeaders, getServerURL } from "@/lib/utils"; +import type { ServerQueryInput } from ".."; + +export async function byOrganisation({ + organisationId, + onSuccess, + onError, +}: { + organisationId: number; +} & ServerQueryInput) { + const url = new URL(`${getServerURL()}/projects/by-organisation`); + url.searchParams.set("organisationId", `${organisationId}`); + + const res = await fetch(url.toString(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to get projects by organisation (${res.status})`); + } 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 new file mode 100644 index 0000000..8594a57 --- /dev/null +++ b/packages/frontend/src/lib/server/project/create.ts @@ -0,0 +1,39 @@ +import { getAuthHeaders, 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}`); + + const res = await fetch(url.toString(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const error = await res.text(); + onError?.(error || `failed to create project (${res.status})`); + } else { + const data = await res.json(); + if (!data.id) { + onError?.(`failed to create project (${res.status})`); + return; + } + + onSuccess?.(data, res); + } +} diff --git a/packages/frontend/src/lib/server/project/index.ts b/packages/frontend/src/lib/server/project/index.ts new file mode 100644 index 0000000..f88382c --- /dev/null +++ b/packages/frontend/src/lib/server/project/index.ts @@ -0,0 +1,2 @@ +export { byOrganisation } from "./byOrganisation"; +export { create } from "./create";