feat: add organization logo upload UI to settings page
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
203349023d
commit
421dc877e2
1 changed files with 104 additions and 0 deletions
|
|
@ -34,6 +34,8 @@ import {
|
|||
useOrganization,
|
||||
useRemoveOrganizationMember,
|
||||
useUpdateOrganization,
|
||||
useUploadOrgLogo,
|
||||
useRemoveOrgLogo,
|
||||
} from "../hooks/organization";
|
||||
import { useRemoveAvatar, useUpdateProfile, useUploadAvatar } from "../hooks/profile";
|
||||
import { useCookieConsent } from "../hooks/useCookieConsent";
|
||||
|
|
@ -59,6 +61,9 @@ export default function SettingsPage() {
|
|||
useInviteOrganizationUser();
|
||||
const { mutate: removeOrganizationMember, isPending: removeOrganizationMemberPending } =
|
||||
useRemoveOrganizationMember();
|
||||
const { mutate: uploadOrgLogo, isPending: uploadOrgLogoPending } = useUploadOrgLogo();
|
||||
const { mutate: removeOrgLogo, isPending: removeOrgLogoPending } = useRemoveOrgLogo();
|
||||
const orgLogoInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [firstName, setFirstName] = useState(user?.first_name || "");
|
||||
const [lastName, setLastName] = useState(user?.last_name || "");
|
||||
|
|
@ -192,6 +197,43 @@ export default function SettingsPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleOrgLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.add({
|
||||
title: t("settings:toasts.error"),
|
||||
description: t("settings:toasts.invalidImage"),
|
||||
type: "error",
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate minimum size client-side
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(img.src);
|
||||
if (img.width < 512 || img.height < 512) {
|
||||
toast.add({
|
||||
title: t("settings:toasts.error"),
|
||||
description: "L'image doit faire au moins 512x512 pixels",
|
||||
type: "error",
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
uploadOrgLogo(file);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
|
||||
// Reset input to allow selecting same file again
|
||||
if (orgLogoInputRef.current) {
|
||||
orgLogoInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container max-w-3xl mx-auto py-6 px-4">
|
||||
|
|
@ -372,6 +414,68 @@ export default function SettingsPage() {
|
|||
<CardDescription>{t("settings:organization.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Organization Logo */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("settings:organization.logo", "Logo de l'organisation")}</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
{organizationData?.organization?.logo_url ? (
|
||||
<img
|
||||
src={`/api/v1/public/org-icons/${organizationData.organization.id}/icon-192.png`}
|
||||
alt="Organization logo"
|
||||
className="w-16 h-16 rounded-lg object-cover ring-2 ring-gray-100 dark:ring-gray-800"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-lg bg-muted flex items-center justify-center ring-2 ring-gray-100 dark:ring-gray-800">
|
||||
<UploadIcon className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={orgLogoInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
onChange={handleOrgLogoChange}
|
||||
hidden
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={uploadOrgLogoPending}
|
||||
onClick={() => orgLogoInputRef.current?.click()}
|
||||
className="gap-2"
|
||||
>
|
||||
{uploadOrgLogoPending ? (
|
||||
<Loader2Icon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<UploadIcon className="w-4 h-4" />
|
||||
)}
|
||||
{organizationData?.organization?.logo_url
|
||||
? t("settings:organization.changeLogo", "Changer")
|
||||
: t("settings:organization.uploadLogo", "Uploader")}
|
||||
</Button>
|
||||
{organizationData?.organization?.logo_url && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={removeOrgLogoPending}
|
||||
onClick={() => removeOrgLogo()}
|
||||
className="gap-2 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950"
|
||||
>
|
||||
{removeOrgLogoPending ? (
|
||||
<Loader2Icon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2Icon className="w-4 h-4" />
|
||||
)}
|
||||
{t("settings:organization.removeLogo", "Supprimer")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings:organization.logoHint", "PNG, JPEG ou WebP, minimum 512x512 pixels")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="organizationName">{t("settings:organization.name")}</Label>
|
||||
<Input
|
||||
|
|
|
|||
Loading…
Reference in a new issue