Add category image upload with thumbnail backdrop in category tiles

- Add category image API methods (upload/delete) to src/api/index.js
- Add uploadCategoryImage/deleteCategoryImage thunks and extraReducers in categorySlice
- Add CategoryBackdrop component showing ThumbHash placeholder with full thumb overlay
- Add photo upload/delete tab to CategoryEditTile settings cycle
- Category tile label band uses semi-transparent black overlay with white text
- Pass thumbHash/fullHash to both CategoryTile and CategoryEditTile
This commit is contained in:
seb
2026-08-03 07:57:44 +02:00
parent 4bbe9caf55
commit 404a7f4fce
4 changed files with 257 additions and 58 deletions

View File

@@ -238,6 +238,38 @@ export const toggleCategoryPos = createAsyncThunk(
},
);
/**
* POST /api/category/:id/image → 201 { category: { id, fullHash, thumbW, thumbH, thumbHash } }
*/
export const uploadCategoryImage = createAsyncThunk(
'category/uploadImage',
async ({ categoryId, file }, { rejectWithValue }) => {
try {
const data = await category.uploadImage(categoryId, file);
requireObject(data.category, 'POST /api/category/:id/image');
return data.category;
} catch (err) {
return catchBackendError(rejectWithValue)(err);
}
},
);
/**
* DELETE /api/category/:id/image → 200 { category: { id, thumbW: null, thumbH: null, thumbHash: null } }
*/
export const deleteCategoryImage = createAsyncThunk(
'category/deleteImage',
async (categoryId, { rejectWithValue }) => {
try {
const data = await category.deleteImage(categoryId);
requireObject(data.category, 'DELETE /api/category/:id/image');
return data.category;
} catch (err) {
return catchBackendError(rejectWithValue)(err);
}
},
);
const initialState = {
companyId: null,
languageId: null,
@@ -442,6 +474,30 @@ const categorySlice = createSlice({
state.categories[key] = { ...existing, pos: updated.pos };
}
})
.addCase(uploadCategoryImage.fulfilled, (state, action) => {
const { id, fullHash, thumbW, thumbH, thumbHash } = action.payload;
if (id == null || state.categories == null) return;
const key = String(id);
const existing = state.categories[key];
if (existing) {
state.categories[key] = { ...existing, fullHash, thumbW, thumbH, thumbHash };
}
})
.addCase(deleteCategoryImage.fulfilled, (state, action) => {
const { id } = action.payload;
if (id == null || state.categories == null) return;
const key = String(id);
const existing = state.categories[key];
if (existing) {
state.categories[key] = {
...existing,
fullHash: null,
thumbW: null,
thumbH: null,
thumbHash: null,
};
}
})
.addCase(loadCurrentCompany.fulfilled, (state, action) => {
const nextCompanyId = action.payload?.id ?? null;
if (nextCompanyId != null && nextCompanyId !== state.companyId) {