From ba08005c9fdd48e576ff8afe5beafb60e48d042a Mon Sep 17 00:00:00 2001 From: QamarQ Date: Sat, 14 Dec 2024 23:19:04 +0100 Subject: [PATCH] fix: fix leaky abstraction --- .../src/app/plans/edit/[id]/page.client.tsx | 64 +++++++++++++++---- frontend/src/lib/usePlan.ts | 29 +++++---- frontend/src/lib/utils/createOnlinePlan.ts | 35 +++++++--- frontend/src/lib/utils/syncPlan.ts | 49 +++++++------- frontend/src/lib/utils/updateLocalPlan.ts | 48 +++++++++----- 5 files changed, 147 insertions(+), 78 deletions(-) diff --git a/frontend/src/app/plans/edit/[id]/page.client.tsx b/frontend/src/app/plans/edit/[id]/page.client.tsx index 18e976c..d27d71e 100644 --- a/frontend/src/app/plans/edit/[id]/page.client.tsx +++ b/frontend/src/app/plans/edit/[id]/page.client.tsx @@ -114,28 +114,66 @@ export function CreateNewPlanPage({ const handleCreateOnlinePlan = async () => { firstTime.current = false; - const result = await createOnlinePlan(plan); - if ("error" in result) { - if (result.error === "NOT_LOGGED_IN") { - setOfflineAlert(true); - } else { - toast.error("Nie udało się utworzyć planu w wersji online", { - description: result.message, - duration: 10000, - }); - } - } else if ("success" in result) { + const res = await createOnlinePlan(plan); + + if (res.status === "SUCCESS") { + const { updatedAt, onlineId } = res; + plan.setPlan((prev) => ({ + ...prev, + synced: true, + updatedAt: new Date(updatedAt), + onlineId: onlineId, + })); + toast.success("Utworzono plan"); + } else if (res.status === "NOT_LOGGED_IN") { + setOfflineAlert(true); + } else { + toast.error("Nie udało się utworzyć planu w wersji online", { + description: res.message, + duration: 10000, + }); } }; const handleSyncPlan = async () => { - await syncPlan(plan, refetchOnlinePlan, setSyncing); + setSyncing(true); + const res = await syncPlan(plan); + + if (res.status === "SUCCESS") { + await refetchOnlinePlan(); + + plan.setPlan((prev) => ({ + ...prev, + synced: true, + updatedAt: res.updatedAt ? new Date(res.updatedAt) : new Date(), + })); + } else { + toast.error(res.message, { + duration: 10000, + }); + } }; const handleUpdateLocalPlan = async () => { firstTime.current = false; - await updateLocalPlan(onlinePlan, plan, coursesFn); + const res = await updateLocalPlan(onlinePlan, coursesFn); + + if (res.status === "SUCCESS") { + const { updatedRegistrations, updatedCourses, updatedAt } = res; + plan.setPlan({ + ...plan, + registrations: updatedRegistrations, + courses: updatedCourses, + synced: true, + toCreate: false, + updatedAt, + }); + } else { + toast.error(res.message, { + duration: 10000, + }); + } }; const bounceAlert = () => { diff --git a/frontend/src/lib/usePlan.ts b/frontend/src/lib/usePlan.ts index fb39e0b..37b6ace 100644 --- a/frontend/src/lib/usePlan.ts +++ b/frontend/src/lib/usePlan.ts @@ -1,5 +1,6 @@ import type { SetStateAction } from "jotai"; import { useAtom } from "jotai"; +import type { Dispatch } from "react"; import { type ExtendedCourse, planFamily } from "@/atoms/planFamily"; @@ -16,21 +17,23 @@ export interface PlanState { toCreate: boolean; createdAt: Date; updatedAt: Date; - setPlan: ( - args_0: SetStateAction<{ - id: string; - name: string; - courses: ExtendedCourse[]; - registrations: Registration[]; - createdAt: Date; - updatedAt: Date; - onlineId: string | null; - toCreate: boolean; - synced: boolean; - }>, - ) => void; + setPlan: setPlanType; } +export type setPlanType = Dispatch< + SetStateAction<{ + id: string; + name: string; + courses: ExtendedCourse[]; + registrations: Registration[]; + createdAt: Date; + updatedAt: Date; + onlineId: string | null; + toCreate: boolean; + synced: boolean; + }> +>; + export const usePlan = ({ planId }: { planId: string }) => { const [plan, setPlan] = useAtom(planFamily({ id: planId })); diff --git a/frontend/src/lib/utils/createOnlinePlan.ts b/frontend/src/lib/utils/createOnlinePlan.ts index 9f02e79..4bce3c1 100644 --- a/frontend/src/lib/utils/createOnlinePlan.ts +++ b/frontend/src/lib/utils/createOnlinePlan.ts @@ -1,7 +1,25 @@ import { createNewPlan } from "@/actions/plans"; import type { PlanState } from "@/lib/usePlan"; -export const createOnlinePlan = async (plan: PlanState) => { +type CreateOnlinePlanResult = + | { + status: "NOT_LOGGED_IN" | "UNKNOWN"; + message: string; + } + | { + status: "SUCCESS"; + updatedAt: Date; + onlineId: string; + }; + +/** + * Creates a new plan online in database on user account. + * @param plan **plan** object from useAtom() + * @returns Objects: ```{ status: "NOT_LOGGED_IN" | "UNKNOWN", message: string }``` or ```{ status: "SUCCESS" }``` + */ +export const createOnlinePlan = async ( + plan: PlanState, +): Promise => { try { const courses = plan.courses .filter((c) => c.isChecked) @@ -18,21 +36,18 @@ export const createOnlinePlan = async (plan: PlanState) => { groups, }); - plan.setPlan((prev) => ({ - ...prev, - synced: true, + return { + status: "SUCCESS", updatedAt: new Date(res.schedule.updatedAt), onlineId: res.schedule.id.toString(), - })); - - return { success: true }; + }; } catch (err) { if (err instanceof Error && "message" in err) { if (err.message === "Not logged in") { - return { error: "NOT_LOGGED_IN", message: err.message }; + return { status: "NOT_LOGGED_IN", message: err.message }; } - return { error: "UNKNOWN", message: err.message }; + return { status: "UNKNOWN", message: err.message }; } - return { error: "UNKNOWN", message: "Wystąpił nieoczekiwany błąd" }; + return { status: "UNKNOWN", message: "Wystąpił nieoczekiwany błąd" }; } }; diff --git a/frontend/src/lib/utils/syncPlan.ts b/frontend/src/lib/utils/syncPlan.ts index 0f72645..855918b 100644 --- a/frontend/src/lib/utils/syncPlan.ts +++ b/frontend/src/lib/utils/syncPlan.ts @@ -1,14 +1,23 @@ -import { toast } from "sonner"; - import { updatePlan } from "@/actions/plans"; import type { PlanState } from "@/lib/usePlan"; -export const syncPlan = async ( - plan: PlanState, - refetchOnlinePlan: () => Promise, - setSyncing: (value: boolean) => void, -): Promise => { - setSyncing(true); +type SyncPlanResult = + | { + status: "ERROR"; + message: string; + } + | { + status: "SUCCESS"; + updatedAt: string; + }; + +/** + * Updates a plan online in database on user account. + * @param plan **plan** object from useAtom() + * @returns Objects ```{ status: "ERROR", message: string }``` or ```{ status: "SUCCESS", updatedAt: string }``` + */ + +export const syncPlan = async (plan: PlanState): Promise => { try { const res = await updatePlan({ id: Number(plan.onlineId), @@ -23,24 +32,14 @@ export const syncPlan = async ( }); if (!res.success) { - toast.error("Nie udało się zaktualizować planu"); - return null; + return { + status: "ERROR", + message: "Nie udało się zaktualizować planu", + }; } - await refetchOnlinePlan(); - - plan.setPlan((prev) => ({ - ...prev, - synced: true, - updatedAt: res.schedule.updatedAt - ? new Date(res.schedule.updatedAt) - : new Date(), - })); - } catch { - toast.error("Nie udało się zaktualizować planu"); - return null; - } finally { - setSyncing(false); + return { status: "SUCCESS", updatedAt: res.schedule.updatedAt }; + } catch (err) { + return { status: "ERROR", message: "Nie udało się zaktualizować planu" }; } - return null; }; diff --git a/frontend/src/lib/utils/updateLocalPlan.ts b/frontend/src/lib/utils/updateLocalPlan.ts index 011f075..4b8f5d5 100644 --- a/frontend/src/lib/utils/updateLocalPlan.ts +++ b/frontend/src/lib/utils/updateLocalPlan.ts @@ -1,22 +1,40 @@ import type { UseMutationResult } from "@tanstack/react-query"; -import { toast } from "sonner"; import type { ExtendedCourse, ExtendedGroup } from "@/atoms/planFamily"; -import type { PlanState } from "@/lib/usePlan"; import type { LessonType } from "@/services/usos/types"; import type { CourseType, PlanResponseType } from "@/types"; +import type { Registration } from "../types"; + +type UpdateLocalPlanResult = + | { + status: "ERROR"; + message: string; + } + | { + status: "SUCCESS"; + updatedRegistrations: Registration[]; + updatedCourses: ExtendedCourse[]; + updatedAt: Date; + }; + +/** + * Updates local plan with online plan data. + * @param onlinePlan Online plan data + * @param coursesFn Function to fetch courses + * @returns Objects ```{ status: "ERROR", message: string }``` or ```{ status: "SUCCESS", updatedRegistrations: Registration[], updatedCourses: ExtendedCourse[], updatedAt: Date }``` + */ + export const updateLocalPlan = async ( onlinePlan: PlanResponseType | null | undefined, - plan: PlanState, coursesFn: UseMutationResult, -): Promise => { +): Promise => { if (!onlinePlan) { - return false; + return { status: "ERROR", message: "Nie udało się pobrać planu online" }; } - let updatedRegistrations: typeof plan.registrations = []; - let updatedCourses: typeof plan.courses = []; + let updatedRegistrations: Registration[] = []; + let updatedCourses: ExtendedCourse[] = []; for (const registration of onlinePlan.registrations) { try { @@ -64,18 +82,14 @@ export const updateLocalPlan = async ( array.findIndex((t) => t.id === course.id) === index, ); } catch { - toast.error("Nie udało się pobrać kursów"); + return { status: "ERROR", message: "Nie udało się pobrać kursów" }; } } - plan.setPlan({ - ...plan, - registrations: updatedRegistrations, - courses: updatedCourses, - synced: true, - toCreate: false, + return { + status: "SUCCESS", + updatedRegistrations, + updatedCourses, updatedAt: new Date(onlinePlan.updatedAt), - }); - - return true; + }; };