clodes / frontend /src /useStore.js
nimzuk's picture
CI: sync from GitHub
20a6f91 verified
// frontend/src/useStore.js
import { create } from "zustand";
// небольшие хелперы для запросов
const postJSON = async (url, body) => {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
};
const postFile = async (url, file) => {
const fd = new FormData();
fd.append("file", file);
const res = await fetch(url, { method: "POST", body: fd });
if (!res.ok) throw new Error(await res.text());
return res.json();
};
const createInitialDetail = () => ({
previewUrl: null,
transform: { scale: 1, tx: 0, ty: 0 },
tile: { enabled: false },
});
const useStore = create((set, get) => ({
// состояние
model: "MT",
active: "front",
details: {
front: createInitialDetail(),
sleeveL: createInitialDetail(),
back: createInitialDetail(),
sleeveR: createInitialDetail(),
},
uploadedPath: null,
uploadedUrl: null,
busy: false,
lastOrderInfo: null,
// сеттеры
setModel: (model) => set({ model }),
setActive: (active) => set({ active }),
setScale: (scale) =>
set((state) => ({
details: {
...state.details,
[state.active]: {
...state.details[state.active],
transform: {
...state.details[state.active].transform,
scale,
},
},
},
})),
setTx: (tx) =>
set((state) => ({
details: {
...state.details,
[state.active]: {
...state.details[state.active],
transform: {
...state.details[state.active].transform,
tx,
},
},
},
})),
setTy: (ty) =>
set((state) => ({
details: {
...state.details,
[state.active]: {
...state.details[state.active],
transform: {
...state.details[state.active].transform,
ty,
},
},
},
})),
toggleTile: () =>
set((state) => ({
details: {
...state.details,
[state.active]: {
...state.details[state.active],
tile: {
enabled: !state.details[state.active].tile.enabled,
},
},
},
})),
// 1) загрузка принта
async upload(file) {
set({ busy: true });
try {
const { path, url } = await postFile("/api/upload", file);
set({ uploadedPath: path, uploadedUrl: url });
return path;
} finally {
set({ busy: false });
}
},
// 2) построение превью из текущих контролов
async spread() {
const { model, active, details, uploadedPath } = get();
const detail = details[active];
if (!uploadedPath) throw new Error("Сначала загрузите принт");
const detail = details[active];
set({ busy: true });
try {
const payload = {
model,
view: active,
details: {
print_path: uploadedPath,
tile: detail.tile.enabled,
offset_x: detail.transform.tx,
offset_y: detail.transform.ty,
scale: detail.transform.scale,
},
};
const data = await postJSON("/api/preview", payload);
const urls =
data.previews ||
(data.preview_url ? { [active]: data.preview_url } : null);
if (urls) {
set((state) => {
const updatedDetails = { ...state.details };
for (const [key, url] of Object.entries(urls)) {
if (!updatedDetails[key]) continue;
updatedDetails[key] = {
...updatedDetails[key],
previewUrl: url,
};
}
return { details: updatedDetails, lastPreviewUrls: urls };
});
} else {
set({ lastPreviewUrls: urls });
}
return data;
} finally {
set({ busy: false });
}
},
// удобный комбинированный экшен под кнопку "Upload / Spread"
async uploadAndSpread(file) {
await get().upload(file);
return get().spread();
},
// 3) создание заказа
async startOrder() {
const { model, active, details, uploadedPath } = get();
const detail = details[active];
if (!uploadedPath) throw new Error("Сначала загрузите принт");
const detail = details[active];
set({ busy: true });
try {
const payload = {
model,
view: active,
details: {
print_path: uploadedPath,
tile: detail.tile.enabled,
offset_x: detail.transform.tx,
offset_y: detail.transform.ty,
scale: detail.transform.scale,
},
};
const data = await postJSON("/api/order", payload);
set({ lastOrderInfo: data });
return data;
} finally {
set({ busy: false });
}
},
}));
export default useStore;