我們昨天大概看了一下探索課程頁
今天我們來看看「ProgramCollection」和 「ProgramCard」這兩個元件
import Tracking from 'lodestar-app-element/src/components/common/Tracking'
// 中間省略
const ProgramCollection: React.FC<{
programs: (ProgramBriefProps & {
supportLocales: string[] | null
categories: Category[]
roles: ProgramRole[]
plans: ProgramPlan[]
})[]
}> = ({ programs }) => {
const { id: appId } = useApp()
const { isAuthenticating } = useAuth()
const tracking = useTracking()
const [type] = useQueryParam('type', StringParam)
const [noPrice] = useQueryParam('noPrice', BooleanParam)
const [noMeta] = useQueryParam('noMeta', BooleanParam)
const { resourceCollection } = useResourceCollection(
appId ? programs.map(program => `${appId}:program:${program.id}`) : [],
true,
)
return (
<div className="row">
<Tracking.Impression resources={resourceCollection} />
{programs.map((program, idx) => (
<div key={program.id} className="col-12 col-md-6 col-lg-4 mb-4">
<ProgramCard
program={program}
programType={type}
noPrice={!!noPrice}
withMeta={!noMeta}
onClick={() => {
const resource = resourceCollection[idx]
resource && tracking.click(resource, { position: idx + 1 })
}}
previousPage={isAuthenticating ? `programs` : undefined}
/>
</div>
))}
</div>
)
}
export default ProgramCollection
在 ProgramCollection 中,可以發現到有一個「Tracking」的元件
他的用途是在於 GA 和 GTM 的數據追蹤
return 的部分,則是回傳「ProgramCard」的元件的陣列
import { Icon } from '@chakra-ui/react'
// 中間省略
const InstructorPlaceHolder = styled.div`
height: 2rem;
`
const StyledWrapper = styled.div<{ variant?: ProgramCardVariant }>`
// 中間省略
`
const StyledContentBlock = styled.div<{ variant?: ProgramCardVariant }>`
// 中間省略
`
const StyledTitle = styled.div<{ variant?: ProgramCardVariant }>`
// 中間省略
`
const StyledReviewRating = styled.div`
// 中間省略
`
const StyledDescription = styled.div`
// 中間省略
`
const StyledMetaBlock = styled.div`
// 中間省略
`
const StyledExtraBlock = styled.div`
// 中間省略
`
const StyledCategoryName = styled.span`
// 中間省略
`
const StyledIcon = styled(Icon)`
color: ${props => props.theme['@primary-color']};
`
type ProgramCardVariant = 'primary' | 'secondary'
type ProgramCardProps = {
program: ProgramBriefProps & {
// 中間省略
}
type SharedProps = {
programLink: string
}
const ProgramCard: React.VFC<ProgramCardProps> = programCardProps => {
const { program, variant, programType, previousPage } = programCardProps
const { settings } = useApp()
const mergedVariant = variant || settings['feature.program_card.variant']
const programLink =
programType && previousPage
? `/programs/${program.id}?type=${programType}&back=${previousPage}`
: programType
? `/programs/${program.id}?type=${programType}`
: previousPage
? `/programs/${program.id}?back=${previousPage}`
: `/programs/${program.id}`
switch (mergedVariant) {
case 'primary':
return <PrimaryCard {...programCardProps} programLink={programLink} />
case 'secondary':
return <SecondaryCard {...programCardProps} programLink={programLink} />
default:
return <PrimaryCard {...programCardProps} programLink={programLink} />
}
}
// 下面略
export default ProgramCard
在 ProgramCard 並沒有太多設定
主要顯示的元件落在「PrimaryCard」和「SecondaryCard」這兩個元件上
以這個範例來說,他並沒有帶入任何 variant
所以預設顯示「PrimaryCard」
const PrimaryCard: React.VFC<ProgramCardProps & SharedProps> = ({
program,
variant = 'primary',
noInstructor,
noPrice,
noTotalDuration,
withMeta,
onClick,
renderCover,
renderCustomDescription,
programLink,
}) => {
const { formatMessage } = useIntl()
const { currentMemberId, currentUserRole } = useAuth()
const { productEditorIds } = useProductEditorIds(program.id)
const { enabledModules, settings } = useApp()
const history = useHistory()
const instructorId = program.roles.length > 0 && program.roles[0].memberId
const listPrice = program.plans[0]?.listPrice || 0
const salePrice =
program.plans.length > 1 && (program.plans[0]?.soldAt?.getTime() || 0) > Date.now()
? program.plans[0]?.salePrice
: (program.plans[0]?.soldAt?.getTime() || 0) > Date.now()
? program.plans[0]?.salePrice
: undefined
const periodAmount = program.plans.length > 1 ? program.plans[0]?.periodAmount : null
const periodType = program.plans.length > 1 ? program.plans[0]?.periodType : null
const { averageScore, reviewCount } = useReviewAggregate(`/programs/${program.id}`)
const { data: enrolledCount } = useProgramEnrollmentAggregate(program.id, { skip: !program.isEnrolledCountVisible })
return (
<>
{!noInstructor && instructorId && (
<InstructorPlaceHolder className="mb-3">
<Link to={`/creators/${instructorId}?tabkey=introduction`}>
<MemberAvatar memberId={instructorId} withName />
</Link>
</InstructorPlaceHolder>
)}
<StyledWrapper
onClick={() => {
onClick && onClick()
history.push(programLink)
}}
>
{renderCover ? (
renderCover(program.coverThumbnailUrl || program.coverUrl || program.coverMobileUrl || EmptyCover)
) : (
<CustomRatioImage
width="100%"
ratio={9 / 16}
src={program.coverThumbnailUrl || program.coverUrl || program.coverMobileUrl || EmptyCover}
/>
)}
<StyledContentBlock variant={variant}>
<StyledTitle variant={variant}>
<Link to={programLink} onClick={onClick}>
{program.title}
</Link>
</StyledTitle>
{enabledModules.customer_review ? (
currentUserRole === 'app-owner' ||
(currentMemberId && productEditorIds.includes(currentMemberId)) ||
reviewCount >= (settings.review_lower_bound ? Number(settings.review_lower_bound) : 3) ? (
<StyledReviewRating className="d-flex mb-2">
<StarRating score={Math.round((Math.round(averageScore * 10) / 10) * 2) / 2} max={5} size="20px" />
<span>({formatMessage(programMessages.ProgramCard.reviewCount, { count: reviewCount })})</span>
</StyledReviewRating>
) : (
<StyledReviewRating className="mb-2">
{formatMessage(programMessages.ProgramCard.noReviews)}
</StyledReviewRating>
)
) : null}
{renderCustomDescription && renderCustomDescription()}
<StyledDescription>{program.abstract}</StyledDescription>
{withMeta && (
<StyledMetaBlock className="d-flex flex-row-reverse justify-content-between align-items-center">
{!noPrice && (
<div>
{program.plans.length === 0 ? (
<span>{formatMessage(programMessages.ProgramCard.notForSale)}</span>
) : (
<PriceLabel
variant="inline"
listPrice={listPrice}
salePrice={salePrice}
periodAmount={periodAmount}
periodType={periodType || undefined}
/>
)}
</div>
)}
<StyledExtraBlock>
{program.plans.length === 1 && !noTotalDuration && !!program.totalDuration && (
<div className="d-flex align-items-center">
<Icon mr="1" as={AiOutlineClockCircle} />
{durationFormatter(program.totalDuration)}
</div>
)}
{program.isEnrolledCountVisible && (
<div className="d-flex align-items-center">
<Icon mr="1" as={AiOutlineUser} />
{enrolledCount}
</div>
)}
</StyledExtraBlock>
</StyledMetaBlock>
)}
</StyledContentBlock>
</StyledWrapper>
</>
)
}
以這個圖片為範例
在 PrimaryCard 中,他會顯示課程的封面圖、名稱、標籤和評價
封面圖使用「CustomRatioImage」這個元件
他是使用 styled-components 製作而成
在專案中的許多地方都可以看見這個元件
使用起來非常方便,他可以自定義圖片的寬度和比例
在這邊他使用 100% 的寬度和 16:9 的圖片比例
在評價的部分,是使用「StarRating」的元件
只要傳入「score」和「max」就可以產生評分
也可以調整他的大小
明天我們繼續看課程的簡介頁