diff --git a/frontend/package.json b/frontend/package.json index 46cb00c..d5eb6e8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "name": "frontend", "version": "0.1.0", "private": true, + "proxy": "http://localhost:27017", "type": "module", "dependencies": { "@testing-library/jest-dom": "^5.17.0", diff --git a/frontend/src/api/requests.ts b/frontend/src/api/requests.ts new file mode 100644 index 0000000..1c59beb --- /dev/null +++ b/frontend/src/api/requests.ts @@ -0,0 +1,119 @@ +/** + * Credit to justinyaodu https://github.com/TritonSE/TSE-Fulcrum/blob/main/frontend/src/api.ts + */ + +/** + * A custom type defining which HTTP methods we will handle in this file + */ +type Method = "GET" | "POST" | "PUT"; + +/** + * A wrapper around the built-in `fetch()` function that abstracts away some of + * the low-level details so we can focus on the important parts of each request. + * See https://developer.mozilla.org/en-US/docs/Web/API/fetch for information + * about the Fetch API. + * + * @param method The HTTP method to use + * @param url The URL to request + * @param body The body of the request, or undefined if there is none + * @param headers The headers of the request + * @returns The Response object returned by `fetch() + */ +async function fetchRequest( + method: Method, + url: string, + body: unknown, + headers: Record, +): Promise { + const hasBody = body !== undefined; + + const newHeaders = { ...headers }; + if (hasBody) { + newHeaders["Content-Type"] = "application/json"; + } + + const response = await fetch(url, { + method, + headers: newHeaders, + body: hasBody ? JSON.stringify(body) : undefined, + }); + + return response; +} + +/** + * Throws an error if the given response's status code indicates an error + * occurred, else does nothing. + * + * @param response A response returned by `fetch()` or `fetchRequest()` + * @throws An error if the response was not successful (200-299) or a redirect + * (300-399) + */ +async function assertOk(response: Response): Promise { + if (response.ok) { + return; + } + + let message = `${response.status} ${response.statusText}`; + + try { + const text = await response.text(); + if (text) { + message += ": " + text; + } + } catch (e) { + // skip errors + } + + throw new Error(message); +} + +/** + * Sends a GET request to the provided URL. + * + * @param url The URL to request + * @param headers The headers of the request (optional) + * @returns The Response object returned by `fetch()` + */ +export async function get(url: string, headers: Record = {}): Promise { + // GET requests do not have a body + const response = await fetchRequest("GET", url, undefined, headers); + assertOk(response); + return response; +} + +/** + * Sends a POST request to the provided URL. + * + * @param url The URL to request + * @param body The body of the request, or undefined if there is none + * @param headers The headers of the request (optional) + * @returns The Response object returned by `fetch()` + */ +export async function post( + url: string, + body: unknown, + headers: Record = {}, +): Promise { + const response = await fetchRequest("POST", url, body, headers); + assertOk(response); + return response; +} + +/** + * Sends a PUT request to the provided URL. + * + * @param url The URL to request + * @param body The body of the request, or undefined if there is none + * @param headers The headers of the request (optional) + * @returns The Response object returned by `fetch()` + */ +export async function put( + url: string, + body: unknown, + headers: Record = {}, +): Promise { + const response = await fetchRequest("GET", url, body, headers); + assertOk(response); + return response; +} diff --git a/frontend/src/api/tasks.ts b/frontend/src/api/tasks.ts new file mode 100644 index 0000000..b07e79b --- /dev/null +++ b/frontend/src/api/tasks.ts @@ -0,0 +1,69 @@ +import { post } from "src/api/requests"; + +/** + * Defines the "shape" of a Task object (what fields are present and their types) for + * frontend components to use. This will be the return type of most functions in this + * file. + */ +export interface Task { + _id: string; + title: string; + description?: string; + isChecked: boolean; + dateCreated: Date; +} + +/** + * Defines the shape of JSON that we'll receive from the backend when we ask the API + * for a Task object. That is, when the backend sends us a JSON object representing a + * Task, we expect it to match these fields and types. + * + * The difference between this type and `Task` above is that `dateCreated` is a string + * instead of a Date object. This is because JSON doesn't support Dates, so we use a + * date-formatted string in requests and responses. + */ +interface TaskJSON { + _id: string; + title: string; + description?: string; + isChecked: boolean; + dateCreated: string; +} + +/** + * Converts a Task from JSON that only contains primitive types to our custom + * Task interface. + * + * @param task The JSON representation of the task + * @returns The parsed Task object + */ +function parseTask(task: TaskJSON): Task { + return { + _id: task._id, + title: task.title, + description: task.description, + isChecked: task.isChecked, + dateCreated: new Date(task.dateCreated), + }; +} + +/** + * The expected inputs when we want to create a new Task object. In the MVP, we only + * need to provide the title and optionally the description, but in the course of + * this tutorial you'll likely want to add more fields here. + */ +export interface CreateTaskRequest { + title: string; + description?: string; +} + +export async function createTask(task: CreateTaskRequest): Promise { + const response = await post("/api/task", task); + const json = (await response.json()) as TaskJSON; + + return parseTask(json); +} + +export async function getAllTasks(): Promise { + return []; +}