iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0

我們昨天大概看了一下探索課程頁
今天我們來看看「ProgramCollection」和 「ProgramCard」這兩個元件

ProgramCollection

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」的元件的陣列

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」

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」就可以產生評分
也可以調整他的大小

明天我們繼續看課程的簡介頁


上一篇
Program (1)
下一篇
Program (3)
系列文
從 Open Source 專案學習 React 開發 - 以 lodestar-app 為例30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言