refactor: fix 413 Request Entity too large error when big image files are uploaded (#27285)

* feat: add image resizing endpoint for organization onboarding

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* chore: add translation keys for image upload errors

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: rename endpoint to resizeBase64Image with imageType enum and Zod validation

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: use ImageUploader and BannerUploader components for organization brand onboarding

Replace server-side image resizing endpoint with client-side cropping using existing ImageUploader and BannerUploader components. This approach:
- Avoids sending large base64 payloads to the server (preventing 413 errors)
- Provides better UX with cropping functionality
- Aligns with team onboarding which already uses ImageUploader
- Removes unnecessary server-side code

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Revert formatting change

* fix: correct max file size from 10mb to 5mb in translations

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Add Remove bvutton too

* fix: correct banner dimension, indentation, and import order

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2026-01-29 08:55:13 +00:00
committed by GitHub
co-authored by hariom@cal.com Devin AI
parent 5b048dd868
commit 6acc84661d
2 changed files with 48 additions and 60 deletions
@@ -2,11 +2,12 @@
import { useRouter, useSearchParams } from "next/navigation";
import posthog from "posthog-js";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { ColorPicker, Label } from "@calcom/ui/components/form";
import { BannerUploader, ImageUploader } from "@calcom/ui/components/image-uploader";
import { OnboardingCard } from "../../components/OnboardingCard";
import { OnboardingLayout } from "../../components/OnboardingLayout";
@@ -25,49 +26,24 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
const { organizationDetails, organizationBrand, setOrganizationBrand } = useOnboardingStore();
const { isMigrationFlow, hasTeams } = useMigrationFlow();
const [_logoFile, setLogoFile] = useState<File | null>(null);
const [_bannerFile, setBannerFile] = useState<File | null>(null);
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [bannerPreview, setBannerPreview] = useState<string | null>(null);
const [brandColor, setBrandColor] = useState("#000000");
// Load from store on mount
useEffect(() => {
setLogoPreview(organizationBrand.logo);
setBannerPreview(organizationBrand.banner);
setBrandColor(organizationBrand.color);
}, [organizationBrand]);
const handleLogoChange = (file: File | null) => {
setLogoFile(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result as string;
setLogoPreview(base64);
setOrganizationBrand({ logo: base64 });
};
reader.readAsDataURL(file);
} else {
setLogoPreview(null);
setOrganizationBrand({ logo: null });
}
const handleLogoChange = (newLogo: string) => {
setLogoPreview(newLogo);
setOrganizationBrand({ logo: newLogo });
};
const handleBannerChange = (file: File | null) => {
setBannerFile(file);
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result as string;
setBannerPreview(base64);
setOrganizationBrand({ banner: base64 });
};
reader.readAsDataURL(file);
} else {
setBannerPreview(null);
setOrganizationBrand({ banner: null });
}
const handleBannerChange = (newBanner: string) => {
setBannerPreview(newBanner);
setOrganizationBrand({ banner: newBanner });
};
const handleColorChange = (color: string) => {
@@ -154,21 +130,28 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
/>
)}
</div>
<div className="flex flex-col gap-2">
<Button
color="secondary"
size="sm"
className="w-fit"
onClick={() => document.getElementById("banner-upload")?.click()}>
{t("upload")}
</Button>
<input
id="banner-upload"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleBannerChange(e.target.files?.[0] || null)}
<div className="flex gap-2">
<BannerUploader
id="org-banner-upload"
buttonMsg={t("upload")}
handleAvatarChange={handleBannerChange}
imageSrc={bannerPreview || undefined}
target="banner"
triggerButtonColor="secondary"
height={500}
width={1500}
/>
{bannerPreview && (
<Button
color="minimal"
size="sm"
onClick={() => {
setBannerPreview(null);
setOrganizationBrand({ banner: null });
}}>
{t("remove")}
</Button>
)}
</div>
</div>
<p className="text-subtle text-xs font-normal leading-3">
@@ -188,21 +171,26 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps)
/>
)}
</div>
<div className="flex flex-col gap-2">
<ImageUploader
id="org-logo-upload"
buttonMsg={t("upload")}
handleAvatarChange={handleLogoChange}
imageSrc={logoPreview || undefined}
target="logo"
triggerButtonColor="secondary"
buttonSize="sm"
/>
{logoPreview && (
<Button
color="secondary"
color="minimal"
size="sm"
onClick={() => document.getElementById("logo-upload")?.click()}>
{t("upload")}
onClick={() => {
setLogoPreview(null);
setOrganizationBrand({ logo: null });
}}>
{t("remove")}
</Button>
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleLogoChange(e.target.files?.[0] || null)}
/>
</div>
)}
</div>
<p className="text-subtle text-xs font-normal leading-3">{t("onboarding_logo_size_hint")}</p>
</div>
@@ -4291,8 +4291,8 @@
"onboarding_org_brand_subtitle": "Customize your booking pages with your logo and a custom banner",
"onboarding_primary_color_label": "Primary color",
"onboarding_banner_label": "Banner",
"onboarding_logo_size_hint": "Recommended size 64x64px (max 10mb)",
"onboarding_banner_size_hint": "Recommended size 1500x150px (max 10mb)",
"onboarding_logo_size_hint": "Recommended size 64x64px (max 5mb)",
"onboarding_banner_size_hint": "Recommended size 1500x500px (max 5mb)",
"onboarding_preview_nameless": "Nameless",
"onboarding_preview_example_title": "Enterprise sales",
"onboarding_preview_example_description": "Jump on a call with one of our sales reps to learn more",