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:
Arthur Belleville 2026-04-02 22:05:56 +02:00
parent 203349023d
commit 421dc877e2
No known key found for this signature in database

View file

@ -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