Skip to content

Commit

Permalink
fix: schematic image preview
Browse files Browse the repository at this point in the history
  • Loading branch information
Ekep-Obasi committed Dec 21, 2024
1 parent 8c08f3b commit e7c26ff
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 8 deletions.
167 changes: 167 additions & 0 deletions src/people/SchematicPreviewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable no-use-before-define */
import React from 'react';
import MaterialIcon from '@material/react-material-icon';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { ImageState, SchematicPreviewProps } from './interfaces';

const IMAGE_LOAD_TIMEOUT = 30000;
const DEFAULT_ALT_TEXT = 'Schematic preview';

export const SchematicPreview = memo(function ({
schematicUrl,
schematicImg,
onLoadSuccess,
onLoadError,
className,
testId = 'schematic-preview'
}: SchematicPreviewProps) {
const [imageState, setImageState] = useState<ImageState>('initial');

const imageSource = useMemo(
() => schematicImg || schematicUrl || '',
[schematicImg, schematicUrl]
);

const handleImageLoad = useCallback(() => {
setImageState('loaded');
onLoadSuccess?.();
}, [onLoadSuccess]);

const handleImageError = useCallback(
(error: Error) => {
setImageState('error');
onLoadError?.(error);
},
[onLoadError]
);

useEffect(() => {
if (!imageSource) {
setImageState('initial');
return;
}

setImageState('loading');

const imgElement = new Image();
const timeoutId = setTimeout(() => {
handleImageError(new Error('Image load timeout'));
}, IMAGE_LOAD_TIMEOUT);

imgElement.onload = () => {
clearTimeout(timeoutId);
handleImageLoad();
};

imgElement.onerror = (event: Event | string) => {
clearTimeout(timeoutId);
handleImageError(event instanceof ErrorEvent ? event.error : new Error('Image load failed'));
};

imgElement.src = imageSource;

return () => {
clearTimeout(timeoutId);
imgElement.onload = null;
imgElement.onerror = null;
};
}, [imageSource, handleImageLoad, handleImageError]);

const renderContent = () => {
if (!imageSource) {
return <NoImageText>No image available</NoImageText>;
}

return (
<>
{imageState === 'loading' && <Skeleton />}

{imageState === 'error' && (
<ErrorContainer>
<MaterialIcon icon="error_outline" style={{ fontSize: '24px' }} />
<ErrorText>Failed to load schematic image</ErrorText>
</ErrorContainer>
)}

<StyledImage
src={imageSource}
alt={DEFAULT_ALT_TEXT}
loading="lazy"
isVisible={imageState === 'loaded'}
data-testid={`${testId}-image`}
/>
</>
);
};

return (
<Container className={className} data-testid={testId} role="img" aria-label={DEFAULT_ALT_TEXT}>
{renderContent()}
</Container>
);
});

SchematicPreview.displayName = 'SchematicPreview';

const Container = styled.div`
position: relative;
aspect-ratio: 16/9;
width: 100%;
border-radius: 10px;
overflow: hidden;
background-color: var(--background-secondary);
`;

const Skeleton = styled.div`
position: absolute;
inset: 0;
background: linear-gradient(90deg, #ebedf1 0%, #f2f3f5 50%, #ebedf1 100%);
background-size: 200% 100%;
animation: pulse 2s ease-in-out infinite;
@keyframes pulse {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
`;

const ErrorContainer = styled.div`
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
background-color: var(--background-error);
color: var(--text-error);
`;

const ErrorText = styled.span`
font-size: 14px;
font-weight: 500;
text-align: center;
`;

const NoImageText = styled.span`
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 14px;
`;

const StyledImage = styled.img<{ isVisible: boolean }>`
width: 100%;
height: 100%;
object-fit: cover;
opacity: ${(props: { isVisible: boolean }) => (props.isVisible ? 1 : 0)};
`;
11 changes: 11 additions & 0 deletions src/people/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,14 @@ export interface OfferViewProps {
export interface RenderWidgetsProps {
widget: any;
}

export type ImageState = 'initial' | 'loading' | 'loaded' | 'error';

export interface SchematicPreviewProps {
schematicUrl?: string;
schematicImg?: string;
onLoadSuccess?: () => void;
onLoadError?: (error: Error) => void;
className?: string;
testId?: string;
}
13 changes: 5 additions & 8 deletions src/people/widgetViews/WorkspaceMission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { AvatarGroup } from 'components/common/AvatarGroup';
import { userHasRole } from 'helpers/helpers-extended';
import { CodeGraph, Chat } from 'store/interface';
import { useHistory } from 'react-router-dom';
import { SchematicPreview } from 'people/SchematicPreviewer';
import avatarIcon from '../../public/static/profile_avatar.svg';
import { colors } from '../../config/colors';
import dragIcon from '../../pages/superadmin/header/icons/drag_indicator.svg';
Expand All @@ -69,7 +70,6 @@ import {
RowFlex,
ButtonWrap,
RepoName,
ImgText,
MissionRowFlex,
FullNoBudgetWrap,
FullNoBudgetText
Expand Down Expand Up @@ -1029,13 +1029,10 @@ const WorkspaceMission = () => {
<FieldWrap>
<Label>Schematic</Label>
<Data style={{ border: 'none', paddingLeft: '0px', padding: '5px 5px' }}>
<ImgContainer>
{workspaceData?.schematic_img ? (
<SelectedImg src={workspaceData?.schematic_img} alt="schematic image" />
) : (
<ImgText>Image</ImgText>
)}
</ImgContainer>
<SchematicPreview
schematicImg={workspaceData?.schematic_img || ''}
schematicUrl={workspaceData?.schematic_url || ''}
/>
<RowWrap>
<OptionsWrap style={{ position: 'unset', display: 'contents' }}>
<MaterialIcon
Expand Down

0 comments on commit e7c26ff

Please sign in to comment.