This commit is contained in:
2026-03-26 15:39:58 +03:30
parent e89c3a1b16
commit 93215a7366
3 changed files with 86 additions and 18 deletions
+66 -9
View File
@@ -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<string, string>,
): 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<Record<string, string>>((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<string, string>,
): Promise<T> {
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<T>(response);
@@ -173,12 +227,11 @@ export class ApiClient {
customHeaders?: Record<string, string>,
): Promise<T> {
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<T>(response);
@@ -193,10 +246,12 @@ export class ApiClient {
customHeaders?: Record<string, string>,
): Promise<T> {
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<T>(response);
@@ -211,10 +266,12 @@ export class ApiClient {
customHeaders?: Record<string, string>,
): Promise<T> {
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<T>(response);
@@ -176,7 +176,7 @@ export const farmDashboardService = {
try {
const response = await apiClient.get<
ApiResponse<FarmDashboardConfigResponse> | 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> | FarmDashboardConfigResponse
>("/api/farm-dashboard-config", toApiRequest(data));
>("/api/farm-dashboard-config/", toApiRequest(data));
const raw = response && "data" in response ? response.data : response;
if (
raw &&
@@ -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 }));