Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions apps/web/core/components/common/cover-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 image or skeleton */
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;
} & React.ComponentProps<"img">;

/**
* 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,
...restProps
} = props;

// Show loading skeleton when src is undefined/null and we don't want to show default
if (!src && !showDefaultWhenEmpty) {
return <div className={cn("bg-layer-2 animate-pulse", className)} />;
}

const displayUrl = getCoverImageDisplayURL(src, fallbackUrl);

return <img src={displayUrl} alt={alt} className={cn("object-cover", className)} {...restProps} />;
}
9 changes: 5 additions & 4 deletions apps/web/core/components/profile/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -210,9 +211,9 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="flex w-full flex-col gap-6">
<div className="relative h-44 w-full">
<img
src={getCoverImageDisplayURL(userCover, DEFAULT_COVER_IMAGE_URL)}
className="h-44 w-full rounded-lg object-cover"
<CoverImage
src={userCover}
className="h-44 w-full rounded-lg"
alt={currentUser?.first_name ?? "Cover image"}
/>
<div className="absolute -bottom-6 left-6 flex items-end justify-between">
Expand Down
14 changes: 6 additions & 8 deletions apps/web/core/components/profile/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -101,13 +101,11 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi
</Link>
</div>
)}
<img
src={
getCoverImageDisplayURL(userData?.cover_image_url, "/users/user-profile-cover-default-img.png") ||
"/users/user-profile-cover-default-img.png"
}
<CoverImage
src={userData?.cover_image_url ?? undefined}
alt={userData?.display_name}
className="h-[110px] w-full object-cover"
className="h-[110px] w-full"
showDefaultWhenEmpty
/>
<div className="absolute -bottom-[26px] left-5 h-[52px] w-[52px] rounded-sm">
{userData?.avatar_url && userData?.avatar_url !== "" ? (
Expand Down
8 changes: 4 additions & 4 deletions apps/web/core/components/project/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -206,10 +206,10 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
<div className="relative h-[118px] w-full rounded-t ">
<div className="absolute inset-0 z-[1] bg-gradient-to-t from-black/60 to-transparent" />

<img
src={getCoverImageDisplayURL(project.cover_image_url, DEFAULT_COVER_IMAGE_URL)}
<CoverImage
src={project.cover_image_url}
alt={project.name}
className="absolute left-0 top-0 h-full w-full rounded-t object-cover"
className="absolute left-0 top-0 h-full w-full rounded-t"
/>

<div className="absolute bottom-4 z-[1] flex h-10 w-full items-center justify-between gap-3 px-4">
Expand Down
15 changes: 6 additions & 9 deletions apps/web/core/components/project/create/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -33,13 +32,11 @@ function ProjectCreateHeader(props: Props) {

return (
<div className="group relative h-44 w-full rounded-lg">
{coverImage && (
<img
src={getCoverImageDisplayURL(coverImage, DEFAULT_COVER_IMAGE_URL)}
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
alt={t("project_cover_image_alt")}
/>
)}
<CoverImage
src={coverImage}
alt={t("project_cover_image_alt")}
className="absolute left-0 top-0 h-full w-full rounded-lg"
/>
<div className="absolute left-2.5 top-2.5">
<ProjectTemplateSelect handleModalClose={handleClose} />
</div>
Expand Down
9 changes: 3 additions & 6 deletions apps/web/core/components/project/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -200,11 +201,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
<form onSubmit={handleSubmit(onSubmit)}>
<div className="relative h-44 w-full">
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
<img
src={getCoverImageDisplayURL(coverImage, DEFAULT_COVER_IMAGE_URL)}
alt="Project cover image"
className="h-44 w-full rounded-md object-cover"
/>
<CoverImage src={coverImage} alt="Project cover image" className="h-44 w-full rounded-md" />
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
<div className="flex flex-grow gap-3 truncate">
<Controller
Expand Down
Loading