PluginProbe ʕ •ᴥ•ʔ
Hostinger Reach – AI-Powered Email Marketing for WordPress / 1.0.5
Hostinger Reach – AI-Powered Email Marketing for WordPress v1.0.5
1.5.1 1.5.0 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 trunk 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6
hostinger-reach / frontend / vue / components / UsageCard.vue
hostinger-reach / frontend / vue / components Last commit date
Modals 9 months ago skeletons 9 months ago ActionButton.vue 9 months ago ActionButtonsSection.vue 9 months ago Banner.vue 9 months ago FAQ.vue 9 months ago FormItem.vue 9 months ago FormsSection.vue 9 months ago Hero.vue 9 months ago PluginEntriesTable.vue 9 months ago PluginEntry.vue 9 months ago PluginExpansion.vue 9 months ago Toggle.vue 9 months ago UsageCard.vue 9 months ago UsageCardsRow.vue 9 months ago UsageCardsSection.vue 9 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