Modals
10 months ago
skeletons
10 months ago
ActionButton.vue
10 months ago
ActionButtonsSection.vue
10 months ago
Banner.vue
10 months ago
FAQ.vue
10 months ago
FormItem.vue
10 months ago
FormsSection.vue
10 months ago
Hero.vue
10 months ago
PluginEntriesTable.vue
10 months ago
PluginEntry.vue
10 months ago
PluginExpansion.vue
10 months ago
Toggle.vue
10 months ago
UsageCard.vue
10 months ago
UsageCardsRow.vue
10 months ago
UsageCardsSection.vue
10 months ago
UsageCard.vue
246 lines
| 1 | <script setup lang="ts"> |
| 2 | import { HSkeletonLoader } from '@hostinger/hcomponents'; |
| 3 | import { computed } from 'vue'; |
| 4 | |
| 5 | import { translate } from '@/utils/translate'; |
| 6 | |
| 7 | interface MetricData { |
| 8 | label: string; |
| 9 | value: string | number; |
| 10 | hasIcon?: boolean; |
| 11 | tooltip?: string; |
| 12 | } |
| 13 | |
| 14 | interface Props { |
| 15 | title: string; |
| 16 | layout: 'horizontal' | 'vertical'; |
| 17 | metrics: MetricData[]; |
| 18 | isLoading?: boolean; |
| 19 | } |
| 20 | |
| 21 | const props = withDefaults(defineProps<Props>(), { |
| 22 | isLoading: false |
| 23 | }); |
| 24 | |
| 25 | const isHorizontal = computed(() => props.layout === 'horizontal'); |
| 26 | const isVertical = computed(() => props.layout === 'vertical'); |
| 27 | |
| 28 | const contentClasses = computed(() => ({ |
| 29 | 'usage-card__content': true, |
| 30 | 'usage-card__content--horizontal': isHorizontal.value, |
| 31 | 'usage-card__content--vertical': isVertical.value |
| 32 | })); |
| 33 | |
| 34 | const getMetricClasses = (isVerticalMetric: boolean) => ({ |
| 35 | 'usage-card__metric': true, |
| 36 | 'usage-card__metric--vertical': isVerticalMetric |
| 37 | }); |
| 38 | |
| 39 | const SKELETON_CONFIG = { |
| 40 | title: { height: '24px', width: '120px' }, |
| 41 | label: { height: '16px', width: isVertical.value ? '80px' : '60px' }, |
| 42 | value: { height: '32px', width: isVertical.value ? '50px' : '40px' } |
| 43 | } as const; |
| 44 | </script> |
| 45 | |
| 46 | <template> |
| 47 | <article |
| 48 | class="usage-card" |
| 49 | :class="{ 'usage-card--vertical': isVertical }" |
| 50 | role="article" |
| 51 | :aria-label="`${title} ${translate('hostinger_reach_ui_usage_statistics')}`" |
| 52 | > |
| 53 | <HCard class="usage-card__container" padding="20px 32px" border-radius="16px"> |
| 54 | <div class="usage-card__inner"> |
| 55 | <header class="usage-card__header"> |
| 56 | <HSkeletonLoader |
| 57 | v-if="isLoading" |
| 58 | :height="SKELETON_CONFIG.title.height" |
| 59 | :width="SKELETON_CONFIG.title.width" |
| 60 | rounded |
| 61 | /> |
| 62 | <HText v-else id="usage-card-title" as="h3" variant="heading-3" class="usage-card__title"> |
| 63 | {{ title }} |
| 64 | </HText> |
| 65 | </header> |
| 66 | |
| 67 | <div :class="contentClasses"> |
| 68 | <template v-for="(metric, index) in metrics" :key="index"> |
| 69 | <div :class="getMetricClasses(isVertical)"> |
| 70 | <div class="usage-card__metric-content"> |
| 71 | <div class="usage-card__metric-label-container"> |
| 72 | <HSkeletonLoader |
| 73 | v-if="isLoading" |
| 74 | :height="SKELETON_CONFIG.label.height" |
| 75 | :width="SKELETON_CONFIG.label.width" |
| 76 | rounded |
| 77 | /> |
| 78 | <HText v-else as="div" variant="body-3-secondary" class="usage-card__metric-label"> |
| 79 | {{ metric.label }} |
| 80 | </HText> |
| 81 | <span |
| 82 | v-if="metric.hasIcon && !isLoading" |
| 83 | v-tooltip="metric.tooltip" |
| 84 | class="d-flex align-items-center" |
| 85 | > |
| 86 | <HIcon name="ic-info-circle-filled-16" color="neutral--300" aria-hidden="true" /> |
| 87 | </span> |
| 88 | </div> |
| 89 | |
| 90 | <HSkeletonLoader |
| 91 | v-if="isLoading" |
| 92 | :height="SKELETON_CONFIG.value.height" |
| 93 | :width="SKELETON_CONFIG.value.width" |
| 94 | rounded |
| 95 | /> |
| 96 | <HText v-else as="div" variant="heading-1" class="usage-card__metric-value"> |
| 97 | {{ metric.value }} |
| 98 | </HText> |
| 99 | </div> |
| 100 | </div> |
| 101 | |
| 102 | <div v-if="isHorizontal && index < metrics.length - 1" class="usage-card__divider" aria-hidden="true" /> |
| 103 | </template> |
| 104 | </div> |
| 105 | </div> |
| 106 | </HCard> |
| 107 | </article> |
| 108 | </template> |
| 109 | |
| 110 | <style scoped lang="scss"> |
| 111 | $mobile-breakpoint: 767px; |
| 112 | |
| 113 | .usage-card { |
| 114 | flex: 1; |
| 115 | display: flex; |
| 116 | flex-direction: column; |
| 117 | |
| 118 | &--vertical { |
| 119 | max-width: none; |
| 120 | } |
| 121 | |
| 122 | &__container { |
| 123 | flex: 1; |
| 124 | display: flex; |
| 125 | flex-direction: column; |
| 126 | border: 1px solid var(--neutral--200); |
| 127 | } |
| 128 | |
| 129 | &__inner { |
| 130 | display: flex; |
| 131 | flex-direction: column; |
| 132 | align-self: stretch; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | .usage-card__header { |
| 137 | display: flex; |
| 138 | align-items: center; |
| 139 | align-self: stretch; |
| 140 | gap: 4px; |
| 141 | margin-bottom: 8px; |
| 142 | } |
| 143 | |
| 144 | .usage-card__title { |
| 145 | font-weight: 700; |
| 146 | font-size: 16px; |
| 147 | line-height: 1.5; |
| 148 | color: var(--neutral--700); |
| 149 | } |
| 150 | |
| 151 | .usage-card__content { |
| 152 | display: flex; |
| 153 | align-self: stretch; |
| 154 | |
| 155 | &--horizontal { |
| 156 | flex-direction: row; |
| 157 | align-items: center; |
| 158 | gap: 40px; |
| 159 | } |
| 160 | |
| 161 | &--vertical { |
| 162 | flex-direction: column; |
| 163 | justify-content: space-between; |
| 164 | height: 200px; |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | .usage-card__metric { |
| 169 | display: flex; |
| 170 | align-items: center; |
| 171 | |
| 172 | &:not(&--vertical) { |
| 173 | flex-direction: row; |
| 174 | gap: 40px; |
| 175 | } |
| 176 | |
| 177 | &--vertical { |
| 178 | flex-direction: column; |
| 179 | align-self: stretch; |
| 180 | gap: 16px; |
| 181 | padding: 8px 0 4px; |
| 182 | } |
| 183 | |
| 184 | &-content { |
| 185 | display: flex; |
| 186 | flex-direction: column; |
| 187 | gap: 4px; |
| 188 | |
| 189 | .usage-card__metric--vertical & { |
| 190 | align-self: stretch; |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | &-label-container { |
| 195 | display: flex; |
| 196 | align-items: center; |
| 197 | gap: 8px; |
| 198 | margin-bottom: 4px; |
| 199 | } |
| 200 | |
| 201 | &-label { |
| 202 | font-weight: 400; |
| 203 | font-size: 12px; |
| 204 | line-height: 1.67; |
| 205 | color: var(--neutral--300); |
| 206 | } |
| 207 | |
| 208 | &-value { |
| 209 | font-weight: 700; |
| 210 | font-size: 24px; |
| 211 | line-height: 1.33; |
| 212 | color: var(--neutral--600); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | .usage-card__divider { |
| 217 | width: 1px; |
| 218 | height: 20px; |
| 219 | background-color: var(--neutral--200); |
| 220 | flex-shrink: 0; |
| 221 | } |
| 222 | |
| 223 | @media (max-width: $mobile-breakpoint) { |
| 224 | .usage-card__content--horizontal { |
| 225 | flex-direction: column; |
| 226 | align-items: flex-start; |
| 227 | gap: 16px; |
| 228 | } |
| 229 | |
| 230 | .usage-card__metric:not(.usage-card__metric--vertical) { |
| 231 | flex-direction: column; |
| 232 | align-items: flex-start; |
| 233 | gap: 8px; |
| 234 | width: 100%; |
| 235 | |
| 236 | .usage-card__metric-content { |
| 237 | width: 100%; |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | .usage-card__divider { |
| 242 | display: none; |
| 243 | } |
| 244 | } |
| 245 | </style> |
| 246 |