From 8c0868d8760ed8188db273ad3c914113ec7c2f56 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:26:16 +0530 Subject: [PATCH 1/4] refactor: replace cover image handling with CoverImage component across profile and project forms --- .../core/components/common/cover-image.tsx | 43 +++++++++++++++++++ apps/web/core/components/profile/form.tsx | 9 ++-- apps/web/core/components/profile/sidebar.tsx | 14 +++--- apps/web/core/components/project/card.tsx | 8 ++-- .../core/components/project/create/header.tsx | 15 +++---- apps/web/core/components/project/form.tsx | 9 ++-- 6 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 apps/web/core/components/common/cover-image.tsx diff --git a/apps/web/core/components/common/cover-image.tsx b/apps/web/core/components/common/cover-image.tsx new file mode 100644 index 00000000000..5e0386b3897 --- /dev/null +++ b/apps/web/core/components/common/cover-image.tsx @@ -0,0 +1,43 @@ +import { cn } from "@plane/utils"; +// helpers +import { getCoverImageDisplayURL, DEFAULT_COVER_IMAGE_URL } from "@/helpers/cover-image.helper"; + +type TCoverImageProps = { + /** The cover image URL - can be static, uploaded, or external */ + src: string | null | undefined; + /** Alt text for the image */ + alt?: string; + /** Additional className for the container */ + className?: string; + /** Whether to show default image when src is null/undefined. If false, shows loading skeleton */ + showDefaultWhenEmpty?: boolean; + /** Custom fallback URL to use instead of DEFAULT_COVER_IMAGE_URL */ + fallbackUrl?: string; +}; + +/** + * A reusable cover image component that handles: + * - Loading states with skeleton + * - Static images (local assets) + * - Uploaded images (processed through getFileURL) + * - External URLs + * - Fallback to default cover image + */ +export function CoverImage(props: TCoverImageProps) { + const { + src, + alt = "Cover image", + className, + showDefaultWhenEmpty = false, + fallbackUrl = DEFAULT_COVER_IMAGE_URL, + } = props; + + // Show loading skeleton when src is undefined/null and we don't want to show default + if (!src && !showDefaultWhenEmpty) { + return
; + } + + const displayUrl = getCoverImageDisplayURL(src, fallbackUrl); + + return {alt}; +} diff --git a/apps/web/core/components/profile/form.tsx b/apps/web/core/components/profile/form.tsx index bafd3935f75..f39cd7fcc7e 100644 --- a/apps/web/core/components/profile/form.tsx +++ b/apps/web/core/components/profile/form.tsx @@ -20,8 +20,9 @@ import { DeactivateAccountModal } from "@/components/account/deactivate-account- import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { ChangeEmailModal } from "@/components/core/modals/change-email-modal"; import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal"; +import { CoverImage } from "@/components/common/cover-image"; // helpers -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL, handleCoverImageChange } from "@/helpers/cover-image.helper"; +import { handleCoverImageChange } from "@/helpers/cover-image.helper"; import { captureSuccess, captureError } from "@/helpers/event-tracker.helper"; // hooks import { useInstance } from "@/hooks/store/use-instance"; @@ -210,9 +211,9 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
- {currentUser?.first_name
diff --git a/apps/web/core/components/profile/sidebar.tsx b/apps/web/core/components/profile/sidebar.tsx index 5d987cd5571..1336d42623d 100644 --- a/apps/web/core/components/profile/sidebar.tsx +++ b/apps/web/core/components/profile/sidebar.tsx @@ -17,8 +17,8 @@ import type { IUserProfileProjectSegregation } from "@plane/types"; // plane ui import { Loader } from "@plane/ui"; import { cn, renderFormattedDate, getFileURL } from "@plane/utils"; -// helpers -import { getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; +// components +import { CoverImage } from "@/components/common/cover-image"; // hooks import { useAppTheme } from "@/hooks/store/use-app-theme"; import { useProject } from "@/hooks/store/use-project"; @@ -101,13 +101,11 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi
)} - {userData?.display_name}
{userData?.avatar_url && userData?.avatar_url !== "" ? ( diff --git a/apps/web/core/components/project/card.tsx b/apps/web/core/components/project/card.tsx index c1fe9b7a33c..379bb662687 100644 --- a/apps/web/core/components/project/card.tsx +++ b/apps/web/core/components/project/card.tsx @@ -22,10 +22,10 @@ import { useUserPermissions } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; // local imports +import { CoverImage } from "@/components/common/cover-image"; import { DeleteProjectModal } from "./delete-project-modal"; import { JoinProjectModal } from "./join-project-modal"; import { ArchiveRestoreProjectModal } from "./settings/archive-project/archive-restore-modal"; -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; type Props = { project: IProject; @@ -206,10 +206,10 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
- {project.name}
diff --git a/apps/web/core/components/project/create/header.tsx b/apps/web/core/components/project/create/header.tsx index 0e3a627b692..b201be4fd08 100644 --- a/apps/web/core/components/project/create/header.tsx +++ b/apps/web/core/components/project/create/header.tsx @@ -10,9 +10,8 @@ import type { IProject } from "@plane/types"; // plane ui import { getTabIndex } from "@plane/utils"; // components +import { CoverImage } from "@/components/common/cover-image"; import { ImagePickerPopover } from "@/components/core/image-picker-popover"; -// helpers -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; // plane web imports import { ProjectTemplateSelect } from "@/plane-web/components/projects/create/template-select"; @@ -33,13 +32,11 @@ function ProjectCreateHeader(props: Props) { return (
- {coverImage && ( - {t("project_cover_image_alt")} - )} +
diff --git a/apps/web/core/components/project/form.tsx b/apps/web/core/components/project/form.tsx index 3deb5f66d60..0fd4ad6b518 100644 --- a/apps/web/core/components/project/form.tsx +++ b/apps/web/core/components/project/form.tsx @@ -12,10 +12,11 @@ import { EFileAssetType } from "@plane/types"; import type { IProject, IWorkspace } from "@plane/types"; import { CustomSelect, Input, TextArea } from "@plane/ui"; import { renderFormattedDate } from "@plane/utils"; +import { CoverImage } from "@/components/common/cover-image"; import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { TimezoneSelect } from "@/components/global"; // helpers -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL, handleCoverImageChange } from "@/helpers/cover-image.helper"; +import { handleCoverImageChange } from "@/helpers/cover-image.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; // hooks import { useProject } from "@/hooks/store/use-project"; @@ -200,11 +201,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
- Project cover image +
Date: Mon, 22 Dec 2025 18:39:07 +0530 Subject: [PATCH 2/4] fix: extend CoverImage component to accept additional img props --- apps/web/core/components/common/cover-image.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/core/components/common/cover-image.tsx b/apps/web/core/components/common/cover-image.tsx index 5e0386b3897..70e6d9011a4 100644 --- a/apps/web/core/components/common/cover-image.tsx +++ b/apps/web/core/components/common/cover-image.tsx @@ -13,7 +13,7 @@ type TCoverImageProps = { showDefaultWhenEmpty?: boolean; /** Custom fallback URL to use instead of DEFAULT_COVER_IMAGE_URL */ fallbackUrl?: string; -}; +} & React.ComponentProps<"img">; /** * A reusable cover image component that handles: @@ -30,6 +30,7 @@ export function CoverImage(props: TCoverImageProps) { className, showDefaultWhenEmpty = false, fallbackUrl = DEFAULT_COVER_IMAGE_URL, + ...restProps } = props; // Show loading skeleton when src is undefined/null and we don't want to show default @@ -39,5 +40,5 @@ export function CoverImage(props: TCoverImageProps) { const displayUrl = getCoverImageDisplayURL(src, fallbackUrl); - return {alt}; + return {alt}; } From bac52b5897d11818cae94738c61c6dc1a2576c0f Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:07:09 +0530 Subject: [PATCH 3/4] Update apps/web/core/components/common/cover-image.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/web/core/components/common/cover-image.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/core/components/common/cover-image.tsx b/apps/web/core/components/common/cover-image.tsx index 70e6d9011a4..d0b410bfc30 100644 --- a/apps/web/core/components/common/cover-image.tsx +++ b/apps/web/core/components/common/cover-image.tsx @@ -7,7 +7,7 @@ type TCoverImageProps = { src: string | null | undefined; /** Alt text for the image */ alt?: string; - /** Additional className for the container */ + /** Additional className for the image or skeleton */ className?: string; /** Whether to show default image when src is null/undefined. If false, shows loading skeleton */ showDefaultWhenEmpty?: boolean; From b0460b58fde3294eb956ba5ad28dd2f892a51260 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:04:15 +0530 Subject: [PATCH 4/4] fix: handle undefined cover image URL in ProfileSidebar component --- apps/web/core/components/profile/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/core/components/profile/sidebar.tsx b/apps/web/core/components/profile/sidebar.tsx index 1336d42623d..aa332b6313d 100644 --- a/apps/web/core/components/profile/sidebar.tsx +++ b/apps/web/core/components/profile/sidebar.tsx @@ -102,7 +102,7 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi
)}