diff --git a/src/libs/api/client.ts b/src/libs/api/client.ts index 2b72d02..7547d17 100644 --- a/src/libs/api/client.ts +++ b/src/libs/api/client.ts @@ -28,6 +28,7 @@ export class ApiClient { constructor(baseURL: string = resolveApiBaseUrl()) { this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash this.defaultHeaders = { + Accept: "application/json", "Content-Type": "application/json", }; } @@ -95,6 +96,57 @@ export class ApiClient { return headers; } + private prepareBody( + data: any, + headers: Record, + ): BodyInit | undefined { + if (data === undefined || data === null) return undefined; + + if (typeof FormData !== "undefined" && data instanceof FormData) { + delete headers["Content-Type"]; + return data; + } + + if ( + typeof URLSearchParams !== "undefined" && + data instanceof URLSearchParams + ) { + return data; + } + + if (typeof Blob !== "undefined" && data instanceof Blob) { + return data; + } + + if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) { + return data; + } + + if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(data)) { + return data; + } + + const contentType = headers["Content-Type"]?.toLowerCase(); + + if (contentType?.includes("application/x-www-form-urlencoded")) { + return new URLSearchParams( + Object.entries(data).reduce>((acc, [key, value]) => { + if (value !== undefined && value !== null) { + acc[key] = String(value); + } + + return acc; + }, {}), + ).toString(); + } + + if (!contentType || contentType.includes("application/json")) { + return typeof data === "string" ? data : JSON.stringify(data); + } + + return data as BodyInit; + } + /** * Handle API response */ @@ -155,10 +207,12 @@ export class ApiClient { customHeaders?: Record, ): Promise { const url = `${this.baseURL}${endpoint}`; + const headers = this.getHeaders(customHeaders); + const response = await fetch(url, { method: "POST", - headers: this.getHeaders(customHeaders), - body: data ? JSON.stringify(data) : undefined, + headers, + body: this.prepareBody(data, headers), }); return this.handleResponse(response); @@ -173,12 +227,11 @@ export class ApiClient { customHeaders?: Record, ): Promise { const url = `${this.baseURL}${endpoint}`; - const headers = { ...this.getHeaders(customHeaders) }; - delete headers["Content-Type"]; + const headers = this.getHeaders(customHeaders); const response = await fetch(url, { method: "POST", headers, - body: formData, + body: this.prepareBody(formData, headers), }); return this.handleResponse(response); @@ -193,10 +246,12 @@ export class ApiClient { customHeaders?: Record, ): Promise { const url = `${this.baseURL}${endpoint}`; + const headers = this.getHeaders(customHeaders); + const response = await fetch(url, { method: "PUT", - headers: this.getHeaders(customHeaders), - body: data ? JSON.stringify(data) : undefined, + headers, + body: this.prepareBody(data, headers), }); return this.handleResponse(response); @@ -211,10 +266,12 @@ export class ApiClient { customHeaders?: Record, ): Promise { const url = `${this.baseURL}${endpoint}`; + const headers = this.getHeaders(customHeaders); + const response = await fetch(url, { method: "PATCH", - headers: this.getHeaders(customHeaders), - body: data ? JSON.stringify(data) : undefined, + headers, + body: this.prepareBody(data, headers), }); return this.handleResponse(response); diff --git a/src/libs/api/services/farmDashboardService.ts b/src/libs/api/services/farmDashboardService.ts index c6b61d0..53c5395 100644 --- a/src/libs/api/services/farmDashboardService.ts +++ b/src/libs/api/services/farmDashboardService.ts @@ -176,7 +176,7 @@ export const farmDashboardService = { try { const response = await apiClient.get< ApiResponse | FarmDashboardConfigResponse - >("/api/farm-dashboard-config"); + >("/api/farm-dashboard-config/"); const raw = response && "data" in response ? response.data : response; if ( raw && @@ -202,7 +202,7 @@ export const farmDashboardService = { try { const response = await apiClient.patch< ApiResponse | FarmDashboardConfigResponse - >("/api/farm-dashboard-config", toApiRequest(data)); + >("/api/farm-dashboard-config/", toApiRequest(data)); const raw = response && "data" in response ? response.data : response; if ( raw && diff --git a/src/views/dashboards/farm/FarmDashboardWrapper.tsx b/src/views/dashboards/farm/FarmDashboardWrapper.tsx index 4ae1a98..f498939 100644 --- a/src/views/dashboards/farm/FarmDashboardWrapper.tsx +++ b/src/views/dashboards/farm/FarmDashboardWrapper.tsx @@ -90,6 +90,11 @@ function mergeRowOrderAfterDrag( return result; } +function areStringArraysEqual(left: string[], right: string[]): boolean { + if (left.length !== right.length) return false; + return left.every((value, index) => value === right[index]); +} + const FarmDashboardWrapper = () => { const t = useTranslations("farmDashboard"); const { setSlotContent } = useContext(NavbarSlotContext); @@ -149,7 +154,10 @@ const FarmDashboardWrapper = () => { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const disabledSet = new Set(config.disabledCardIds); + const disabledSet = useMemo( + () => new Set(config.disabledCardIds), + [config.disabledCardIds], + ); const hasVisibleCard = useCallback( (rowId: string) => { @@ -160,7 +168,10 @@ const FarmDashboardWrapper = () => { [config.disabledCardIds], ); - const visibleRowOrder = config.rowOrder.filter(hasVisibleCard); + const visibleRowOrder = useMemo( + () => config.rowOrder.filter(hasVisibleCard), + [config.rowOrder, hasVisibleCard], + ); const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop( visibleRowOrder, @@ -198,18 +209,19 @@ const FarmDashboardWrapper = () => { }, []); useEffect(() => { + if (areStringArraysEqual(orderedRows, visibleRowOrder)) return; setOrderedRows(visibleRowOrder); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visibleRowOrder]); + }, [orderedRows, setOrderedRows, visibleRowOrder]); useEffect(() => { if (loading) return; - if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return; + if (areStringArraysEqual(orderedRows, visibleRowOrder)) return; const newRowOrder = mergeRowOrderAfterDrag( config.rowOrder, orderedRows, visibleRowOrder, ); + if (areStringArraysEqual(newRowOrder, config.rowOrder)) return; setConfig((prev) => ({ ...prev, rowOrder: newRowOrder })); setSaving(true); farmDashboardService @@ -217,8 +229,7 @@ const FarmDashboardWrapper = () => { .then((updated) => setConfig(updated)) .catch(() => {}) .finally(() => setSaving(false)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderedRows]); + }, [config.rowOrder, loading, orderedRows, visibleRowOrder]); const handleToggleDragReorder = useCallback((enabled: boolean) => { setConfig((prev) => ({ ...prev, enableDragReorder: enabled }));