Skip to content

Commit 8895222

Browse files
authored
Merge pull request #720 from sudhanshutech/refactor/card
Refactor Catalog Card and add flip effect
2 parents ab91647 + a6b937f commit 8895222

4 files changed

Lines changed: 724 additions & 0 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
2+
import { Avatar, Typography, styled } from '@mui/material';
3+
import React, { useEffect, useState } from 'react';
4+
import { Grid } from '../../base';
5+
import { CloneIcon, CommunityClassIcon, OfficialClassIcon, OpenIcon, ShareIcon } from '../../icons';
6+
import VerificationClassIcon from '../../icons/ContentClassIcons/VerificationClassIcon';
7+
import DeploymentsIcon from '../../icons/Deployments/DeploymentsIcon';
8+
import { DownloadIcon } from '../../icons/Download';
9+
import { CustomTooltip } from '../CustomTooltip';
10+
import {
11+
CardBack,
12+
CardFront,
13+
DateText,
14+
DateType,
15+
DesignAuthorName,
16+
DesignCard,
17+
DesignDetailsDiv,
18+
DesignInnerCard,
19+
DesignName,
20+
DesignType,
21+
MetricsContainerFront,
22+
MetricsCount,
23+
MetricsDiv,
24+
NoTechnologyText,
25+
ProfileSection,
26+
StyledClassWrapper,
27+
StyledInnerClassWrapper,
28+
TechnologiesSection,
29+
TechnologyText,
30+
VersionTag
31+
} from './style';
32+
33+
export const DesignCardUrl = styled('a')(() => ({
34+
textDecoration: 'none'
35+
}));
36+
37+
interface Pattern {
38+
name: string;
39+
download_count: number;
40+
clone_count: number;
41+
view_count: number;
42+
deployment_count: number;
43+
share_count: number;
44+
userData?: {
45+
version?: string;
46+
avatarUrl?: string;
47+
userName?: string;
48+
technologies?: string[];
49+
updatedAt?: string;
50+
};
51+
catalog_data?: {
52+
content_class?: string;
53+
imageURL?: string;
54+
compatibility?: string[];
55+
};
56+
visibility: string;
57+
updated_at: Date;
58+
}
59+
60+
type CatalogCardProps = {
61+
pattern: Pattern;
62+
patternType: string;
63+
cardLink: string;
64+
cardHeight: string;
65+
cardWidth: string;
66+
cardStyles: React.CSSProperties;
67+
type: string;
68+
version?: string;
69+
avatarUrl: string;
70+
compatibility: string[];
71+
userName: string;
72+
technologies: string[];
73+
updatedAt: string;
74+
shouldFlip?: boolean;
75+
cardTechnologies?: boolean;
76+
isDetailed?: boolean;
77+
cardAvatarUrl?: boolean;
78+
date?: boolean;
79+
cardVersion?: boolean;
80+
UserName?: string;
81+
children?: React.ReactNode; // catalogImage
82+
TechnologyComponent?: React.ReactNode;
83+
basePath?: string; // path of meshmodel img stored
84+
getHostUrl?: () => string;
85+
};
86+
87+
export const ClassToIconMap = {
88+
community: <CommunityClassIcon width="16px" height="12px" />,
89+
official: <OfficialClassIcon width="16px" height="12px" />,
90+
verified: <VerificationClassIcon width="16px" height="12px" />
91+
};
92+
93+
const ClassWrap = ({ catalogClassName }: { catalogClassName: string }) => {
94+
if (!catalogClassName) return <></>;
95+
96+
return (
97+
<StyledClassWrapper>
98+
<StyledInnerClassWrapper catalogClassName={catalogClassName}>
99+
{catalogClassName}
100+
</StyledInnerClassWrapper>
101+
</StyledClassWrapper>
102+
);
103+
};
104+
105+
const CustomCatalogCard: React.FC<CatalogCardProps> = ({
106+
pattern,
107+
patternType,
108+
cardHeight,
109+
cardWidth,
110+
cardStyles,
111+
cardLink,
112+
shouldFlip,
113+
isDetailed,
114+
cardTechnologies,
115+
cardVersion,
116+
avatarUrl,
117+
UserName,
118+
children,
119+
basePath,
120+
getHostUrl
121+
}) => {
122+
const outerStyles = {
123+
height: cardHeight,
124+
width: cardWidth,
125+
...cardStyles
126+
};
127+
128+
const technologies = pattern.catalog_data?.compatibility || []; // an array
129+
const techlimit = 5;
130+
const [availableTechnologies, setAvailableTechnologies] = useState<string[]>([]);
131+
const checkImageUrlValidity = async (url: string, appendHostUrl = true) => {
132+
return new Promise((resolve) => {
133+
const img = new Image();
134+
// Only append host if the URL does not start with "http" or "https"
135+
if (appendHostUrl && !url.startsWith('http')) {
136+
img.src = (getHostUrl ? getHostUrl() : '') + url;
137+
} else {
138+
img.src = url;
139+
}
140+
img.onload = () => {
141+
// Check if the image loaded successfully
142+
resolve(true);
143+
};
144+
145+
img.onerror = () => {
146+
// Handle the case where the image could not be loaded
147+
resolve(false);
148+
};
149+
});
150+
};
151+
152+
const handleImage = async () => {
153+
const validSvgPaths = [];
154+
for (const technology of technologies) {
155+
const svgIconPath = `${basePath}/${technology.toLowerCase()}/icons/color/${technology.toLowerCase()}-color.svg`;
156+
const isSvgPathValid = await checkImageUrlValidity(svgIconPath as string);
157+
if (isSvgPathValid) {
158+
validSvgPaths.push(technology);
159+
}
160+
}
161+
162+
setAvailableTechnologies(validSvgPaths);
163+
};
164+
useEffect(() => {
165+
handleImage();
166+
// eslint-disable-next-line react-hooks/exhaustive-deps
167+
}, []);
168+
169+
return (
170+
<DesignCardUrl href={cardLink} target="_blank" rel="noreferrer">
171+
<DesignCard shouldFlip={shouldFlip} isDetailed={isDetailed} outerStyles={outerStyles}>
172+
<DesignInnerCard shouldFlip={shouldFlip} className="innerCard">
173+
<CardFront shouldFlip={shouldFlip} isDetailed={isDetailed}>
174+
{isDetailed && (
175+
<>
176+
<ClassWrap catalogClassName={pattern?.catalog_data?.content_class ?? ''} />
177+
<DesignType>{patternType}</DesignType>
178+
179+
<DesignName
180+
style={{
181+
color: '#000D12',
182+
margin: '3rem 0 1.59rem 0',
183+
textAlign: 'center'
184+
}}
185+
>
186+
{pattern.name}
187+
</DesignName>
188+
</>
189+
)}
190+
<DesignDetailsDiv>
191+
<div
192+
style={{
193+
background: 'rgba(231, 239, 243, 0.40)',
194+
display: 'flex',
195+
alignItems: 'center',
196+
justifyContent: 'center',
197+
padding: '0.5rem',
198+
width: '100%',
199+
borderRadius: '0.5rem'
200+
}}
201+
>
202+
{children}
203+
</div>
204+
</DesignDetailsDiv>
205+
{isDetailed && (
206+
<MetricsContainerFront isDetailed={isDetailed}>
207+
<MetricsDiv>
208+
<DownloadIcon width={18} height={18} />
209+
<MetricsCount>{pattern.download_count}</MetricsCount>
210+
</MetricsDiv>
211+
<MetricsDiv>
212+
<CloneIcon width={18} height={18} fill={'#51636B'} />
213+
<MetricsCount>{pattern.clone_count}</MetricsCount>
214+
</MetricsDiv>
215+
<MetricsDiv>
216+
<OpenIcon width={18} height={18} fill={'#51636B'} />
217+
<MetricsCount>{pattern.view_count}</MetricsCount>
218+
</MetricsDiv>
219+
<MetricsDiv>
220+
<DeploymentsIcon width={18} height={18} />
221+
<MetricsCount>{pattern.deployment_count}</MetricsCount>
222+
</MetricsDiv>
223+
<MetricsDiv>
224+
<ShareIcon width={18} height={18} fill={'#51636B'} />
225+
<MetricsCount>{pattern.share_count}</MetricsCount>
226+
</MetricsDiv>
227+
</MetricsContainerFront>
228+
)}
229+
</CardFront>
230+
{shouldFlip && (
231+
<CardBack isCatalog={true}>
232+
<ProfileSection>
233+
<Avatar
234+
alt="Design Author"
235+
src={avatarUrl}
236+
sx={{ width: '32px', height: '32px', color: '#293B43' }}
237+
/>
238+
<DesignAuthorName>{UserName}</DesignAuthorName>
239+
</ProfileSection>
240+
241+
<DesignDetailsDiv style={{ marginTop: '0.7rem', gap: '5px' }}>
242+
{cardTechnologies && (
243+
<TechnologiesSection>
244+
<TechnologyText>Technologies</TechnologyText>
245+
<Grid
246+
container
247+
style={{ gap: '4px', alignItems: 'flex-start', flexWrap: 'nowrap' }}
248+
>
249+
{technologies.length < 1 || availableTechnologies.length < 1 ? (
250+
<NoTechnologyText>No technologies</NoTechnologyText>
251+
) : (
252+
<>
253+
{availableTechnologies.slice(0, techlimit).map((technology, index) => {
254+
const svgPath =
255+
(getHostUrl ? getHostUrl() : '') +
256+
`${basePath}/${technology.toLowerCase()}/icons/color/${technology.toLowerCase()}-color.svg`;
257+
return (
258+
<Grid item key={index}>
259+
<CustomTooltip key={index} title={technology.toLowerCase()}>
260+
<img
261+
height="24px"
262+
width="24px"
263+
alt={technology.toLowerCase()}
264+
src={svgPath}
265+
/>
266+
</CustomTooltip>
267+
</Grid>
268+
);
269+
})}
270+
{availableTechnologies.length > techlimit && (
271+
<Grid
272+
item
273+
sx={{
274+
padding: '0 8px 0 4px',
275+
borderRadius: '16px',
276+
border: '1px solid #C9DBE3',
277+
background: '#E7EFF3',
278+
color: '#3C494E',
279+
fontSize: '14px',
280+
lineHeight: '1.5',
281+
fontWeight: '600'
282+
}}
283+
>
284+
+{availableTechnologies.length - techlimit}
285+
</Grid>
286+
)}
287+
</>
288+
)}
289+
</Grid>
290+
</TechnologiesSection>
291+
)}
292+
</DesignDetailsDiv>
293+
294+
{isDetailed && (
295+
<DesignDetailsDiv style={{ marginTop: '50px' }}>
296+
<Grid container style={{ justifyContent: 'space-between', alignItems: 'center' }}>
297+
<Grid
298+
item
299+
style={{
300+
width: '100%',
301+
display: 'flex',
302+
alignItems: 'center',
303+
justifyContent: 'space-between'
304+
}}
305+
>
306+
<div
307+
style={{
308+
display: 'flex',
309+
flexDirection: 'row',
310+
gap: '0.2rem',
311+
alignItems: 'center'
312+
}}
313+
>
314+
<CalendarMonthIcon width={18} height={18} />
315+
<DateType>Updated At</DateType>
316+
</div>
317+
<DateText>
318+
{' '}
319+
{new Date(pattern.updated_at.toString().slice(0, 10)).toLocaleDateString(
320+
'en-US',
321+
{
322+
day: 'numeric',
323+
month: 'long',
324+
year: 'numeric'
325+
}
326+
)}
327+
</DateText>
328+
</Grid>
329+
</Grid>
330+
</DesignDetailsDiv>
331+
)}
332+
333+
{cardVersion && (
334+
<VersionTag>
335+
<Typography variant="body2">v{cardVersion}</Typography>
336+
</VersionTag>
337+
)}
338+
</CardBack>
339+
)}
340+
</DesignInnerCard>
341+
</DesignCard>
342+
</DesignCardUrl>
343+
);
344+
};
345+
346+
export default CustomCatalogCard;

src/custom/CustomCatalog/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import CustomCatalogCard from './custom-card';
2+
3+
export { CustomCatalogCard };

0 commit comments

Comments
 (0)