feat: implement space and workspace icons (#1558)

* feat: implement space and workspace icons
- Create reusable AvatarUploader component supporting avatars, space icons, and workspace icons
- Add Sharp package for server-side image resizing and optimization
- Create reusable AvatarUploader component supporting avatars, space icons, and workspace icons
- Support removing icons

* add workspace logo support
- add upload loader
- add white background to transparent image
- other fixes and enhancements

* dark mode

* fixes

* cleanup
This commit is contained in:
Philip Okugbe
2025-09-15 21:11:37 +01:00
committed by GitHub
parent 61d1cf88a7
commit 1280f96f37
41 changed files with 1043 additions and 213 deletions
@@ -0,0 +1,67 @@
import { useState } from "react";
import { useAtom } from "jotai";
import { Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import AvatarUploader from "@/components/common/avatar-uploader.tsx";
import {
uploadWorkspaceIcon,
removeWorkspaceIcon,
} from "@/features/attachments/services/attachment-service.ts";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import useUserRole from "@/hooks/use-user-role.tsx";
export default function WorkspaceIcon() {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const { isAdmin } = useUserRole();
const handleIconUpload = async (file: File) => {
setIsLoading(true);
try {
const result = await uploadWorkspaceIcon(file);
if (workspace) {
setWorkspace({ ...workspace, logo: result.fileName });
}
} catch (error) {
//
} finally {
setIsLoading(false);
}
};
const handleIconRemove = async () => {
setIsLoading(true);
try {
await removeWorkspaceIcon();
if (workspace) {
setWorkspace({ ...workspace, logo: null });
}
} catch (error) {
//
} finally {
setIsLoading(false);
}
};
return (
<div style={{ marginBottom: "24px" }}>
<Text size="sm" fw={500} mb="xs">
{t("Icon")}
</Text>
<AvatarUploader
currentImageUrl={workspace?.logo}
fallbackName={workspace?.name}
type={AvatarIconType.WORKSPACE_ICON}
size="60px"
radius="sm"
variant="filled"
onUpload={handleIconUpload}
onRemove={handleIconRemove}
isLoading={isLoading}
disabled={!isAdmin}
/>
</div>
);
}
@@ -109,15 +109,3 @@ export async function getAppVersion(): Promise<IVersion> {
return req.data;
}
export async function uploadLogo(file: File) {
const formData = new FormData();
formData.append("type", "workspace-logo");
formData.append("image", file);
const req = await api.post("/attachments/upload-image", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return req.data;
}