From fad4ee8cc74133653263fbf7c7f488175b934f2c Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:38:19 +0300 Subject: [PATCH 1/6] improve upload image ui --- .../organization/upload-organization-icon.ts | 6 +- .../dashboard/settings/account/Settings.tsx | 38 +++--- .../account/components/ProfileImage.tsx | 119 ++++++++++++++++++ .../components/OrganizationIcon.tsx | 2 +- apps/web/components/FileInput.tsx | 30 +++-- 5 files changed, 161 insertions(+), 34 deletions(-) create mode 100644 apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx diff --git a/apps/web/actions/organization/upload-organization-icon.ts b/apps/web/actions/organization/upload-organization-icon.ts index 72fec848e0..64da3c6472 100644 --- a/apps/web/actions/organization/upload-organization-icon.ts +++ b/apps/web/actions/organization/upload-organization-icon.ts @@ -12,6 +12,8 @@ import { revalidatePath } from "next/cache"; import { sanitizeFile } from "@/lib/sanitizeFile"; import { runPromise } from "@/lib/server"; +const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1MB + export async function uploadOrganizationIcon( formData: FormData, organizationId: Organisation.OrganisationId, @@ -47,8 +49,8 @@ export async function uploadOrganizationIcon( } // Validate file size (limit to 2MB) - if (file.size > 2 * 1024 * 1024) { - throw new Error("File size must be less than 2MB"); + if (file.size > MAX_FILE_SIZE_BYTES) { + throw new Error("File size must be less than 1MB"); } // Create a unique file key diff --git a/apps/web/app/(org)/dashboard/settings/account/Settings.tsx b/apps/web/app/(org)/dashboard/settings/account/Settings.tsx index 463e58a1a0..82b81c1d71 100644 --- a/apps/web/app/(org)/dashboard/settings/account/Settings.tsx +++ b/apps/web/app/(org)/dashboard/settings/account/Settings.tsx @@ -16,8 +16,8 @@ import { useEffect, useId, useState } from "react"; import { toast } from "sonner"; import { removeProfileImage } from "@/actions/account/remove-profile-image"; import { uploadProfileImage } from "@/actions/account/upload-profile-image"; -import { FileInput } from "@/components/FileInput"; import { useDashboardContext } from "../../Contexts"; +import { ProfileImage } from "./components/ProfileImage"; import { patchAccountSettings } from "./server"; export const Settings = ({ @@ -32,7 +32,9 @@ export const Settings = ({ const [defaultOrgId, setDefaultOrgId] = useState< Organisation.OrganisationId | undefined >(user?.defaultOrgId || undefined); - const avatarInputId = useId(); + const firstNameId = useId(); + const lastNameId = useId(); + const contactEmailId = useId(); const initialProfileImage = user?.image ?? null; const [profileImageOverride, setProfileImageOverride] = useState< string | null | undefined @@ -163,18 +165,14 @@ export const Settings = ({ }} >
- +
Profile image This image appears in your profile, comments, and shared caps.
-
- - Your name - - Changing your name below will update how your name appears when - sharing a Cap, and in your profile. - -
-
+ +
+ Your name + + Changing your name below will update how your name appears when + sharing a Cap, and in your profile. + +
+
+
setFirstName(e.target.value)} defaultValue={firstName as string} - id="firstName" + id={firstNameId} name="firstName" />
@@ -205,7 +205,7 @@ export const Settings = ({ placeholder="Last name" onChange={(e) => setLastName(e.target.value)} defaultValue={lastName as string} - id="lastName" + id={lastNameId} name="lastName" />
@@ -221,7 +221,7 @@ export const Settings = ({ diff --git a/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx b/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx new file mode 100644 index 0000000000..206624e0bc --- /dev/null +++ b/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { Button } from "@cap/ui"; +import { faImage } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import clsx from "clsx"; +import Image from "next/image"; +import { useRef, useState } from "react"; + +interface ProfileImageProps { + initialPreviewUrl?: string | null; + onChange?: (file: File | null) => void; + onRemove?: () => void; + disabled?: boolean; + isLoading?: boolean; +} + +export function ProfileImage({ + initialPreviewUrl, + onChange, + onRemove, + disabled = false, + isLoading = false, +}: ProfileImageProps) { + const [previewUrl, setPreviewUrl] = useState( + initialPreviewUrl || null, + ); + const fileInputRef = useRef(null); + const [removingImage, setRemovingImage] = useState(false); + + const handleFileChange = () => { + const file = fileInputRef.current?.files?.[0]; + if (file) { + const objectUrl = URL.createObjectURL(file); + setPreviewUrl(objectUrl); + onChange?.(file); + } + }; + + const handleRemove = () => { + setRemovingImage(true); + try { + setPreviewUrl(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + onRemove?.(); + } finally { + setRemovingImage(false); + } + }; + + const handleUploadClick = () => { + if (!disabled && !isLoading && !removingImage) { + fileInputRef.current?.click(); + } + }; + + return ( +
+
+
+ {previewUrl ? ( + Profile Image + ) : ( + + )} +
+ +
+
+ {!removingImage && ( + + )} + {previewUrl && !removingImage && ( + + )} +
+

Recommended size: 120x120

+
+
+
+ ); +} diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx index dc046d28d2..ad4dcc9bc8 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/OrganizationIcon.tsx @@ -31,7 +31,6 @@ export const OrganizationIcon = () => { toast.success("Organization icon updated successfully"); } } catch (error) { - console.error("Error uploading organization icon:", error); toast.error( error instanceof Error ? error.message : "Failed to upload icon", ); @@ -75,6 +74,7 @@ export const OrganizationIcon = () => { isLoading={isUploading} initialPreviewUrl={existingIconUrl || null} onRemove={handleRemoveIcon} + maxFileSizeBytes={1 * 1024 * 1024} // 1MB />
); diff --git a/apps/web/components/FileInput.tsx b/apps/web/components/FileInput.tsx index cf0f4e8610..ecac31dce4 100644 --- a/apps/web/components/FileInput.tsx +++ b/apps/web/components/FileInput.tsx @@ -15,7 +15,7 @@ import { toast } from "sonner"; import { Tooltip } from "./Tooltip"; const ALLOWED_IMAGE_TYPES = new Set(["image/jpeg", "image/png"]); -const MAX_FILE_SIZE_BYTES = 3 * 1024 * 1024; +const DEFAULT_MAX_FILE_SIZE_BYTES = 3 * 1024 * 1024; const ACCEPTED_IMAGE_TYPES = Array.from(ALLOWED_IMAGE_TYPES).join(","); export interface FileInputProps { @@ -23,6 +23,7 @@ export interface FileInputProps { disabled?: boolean; id?: string; name?: string; + containerStyle?: React.CSSProperties; className?: string; notDraggingClassName?: string; initialPreviewUrl?: string | null; @@ -30,10 +31,12 @@ export interface FileInputProps { isLoading?: boolean; height?: string | number; previewIconSize?: string | number; + maxFileSizeBytes?: number; } export const FileInput: React.FC = ({ onChange, + containerStyle, disabled = false, id = "file", name = "file", @@ -42,8 +45,9 @@ export const FileInput: React.FC = ({ initialPreviewUrl = null, onRemove, isLoading = false, - height = "44px", + height = 44, previewIconSize = 20, + maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES, }) => { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); @@ -130,9 +134,10 @@ export const FileInput: React.FC = ({ return; } - // Validate file size (limit to 3MB) - if (file.size > MAX_FILE_SIZE_BYTES) { - toast.error("File size must be 3MB or less"); + // Validate file size + if (file.size > maxFileSizeBytes) { + const maxSizeMB = maxFileSizeBytes / (1024 * 1024); + toast.error(`File size must be ${maxSizeMB}MB or less`); if (fileInputRef.current) { fileInputRef.current.value = ""; } @@ -185,14 +190,15 @@ export const FileInput: React.FC = ({
{/* Fixed height container to prevent resizing */} {previewUrl ? ( -
+
-
-
+
+

Current icon:{" "}

@@ -201,7 +207,7 @@ export const FileInput: React.FC = ({ width: previewIconSize, height: previewIconSize, }} - className="relative flex flex-shrink-0 items-center justify-center overflow-hidden rounded-full" + className="flex overflow-hidden relative flex-shrink-0 justify-center items-center rounded-full" > {previewUrl && ( = ({ )} - {previewUrl && !removingImage && ( + {(previewUrl || isRemoving) && ( )}
From d4313eef52f3bd3ee025c9cdf52222e2ead7bb70 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:04:01 +0300 Subject: [PATCH 3/6] change variant --- .../dashboard/settings/account/components/ProfileImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx b/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx index e3d0af109a..cdf6bb47dd 100644 --- a/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx +++ b/apps/web/app/(org)/dashboard/settings/account/components/ProfileImage.tsx @@ -107,7 +107,7 @@ export function ProfileImage({ {(previewUrl || isRemoving) && ( )} {(previewUrl || isRemoving) && ( - + + + )}

Recommended size: 120x120