@eeplatform/nuxt-layer-common 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +37 -0
  8. package/.playground/nuxt.config.ts +20 -0
  9. package/CHANGELOG.md +7 -0
  10. package/README.md +73 -0
  11. package/app.vue +3 -0
  12. package/components/AddPaymentMethod.vue +585 -0
  13. package/components/BtnUploadFile.vue +139 -0
  14. package/components/ConfirmDialog.vue +66 -0
  15. package/components/Container/Standard.vue +33 -0
  16. package/components/Input/Date.vue +177 -0
  17. package/components/Input/ListGroupSelection.vue +93 -0
  18. package/components/Input/NewDate.vue +123 -0
  19. package/components/Input/Number.vue +124 -0
  20. package/components/Input/Password.vue +35 -0
  21. package/components/InputLabel.vue +18 -0
  22. package/components/InvitationMain.vue +195 -0
  23. package/components/Layout/Header.vue +285 -0
  24. package/components/Layout/NavigationDrawer.vue +52 -0
  25. package/components/LinkHome.vue +9 -0
  26. package/components/ListItem.vue +35 -0
  27. package/components/LocalPagination.vue +41 -0
  28. package/components/MemberMain.vue +452 -0
  29. package/components/NavigationItem.vue +73 -0
  30. package/components/PlaceholderComponent.vue +34 -0
  31. package/components/RolePermissionFormCreate.vue +179 -0
  32. package/components/RolePermissionFormPreviewUpdate.vue +184 -0
  33. package/components/RolePermissionMain.vue +376 -0
  34. package/components/Snackbar.vue +23 -0
  35. package/components/SpecificAttr.vue +57 -0
  36. package/components/Std/Pagination.vue +52 -0
  37. package/components/SwitchContext.vue +109 -0
  38. package/components/SwitchOrg.vue +159 -0
  39. package/components/TableList.vue +130 -0
  40. package/composables/useAddress.ts +144 -0
  41. package/composables/useChartOfAccount.ts +62 -0
  42. package/composables/useCommonPermission.ts +130 -0
  43. package/composables/useFile.ts +29 -0
  44. package/composables/useInvoice.ts +42 -0
  45. package/composables/useLocal.ts +63 -0
  46. package/composables/useLocalAuth.ts +157 -0
  47. package/composables/useLocalSetup.ts +46 -0
  48. package/composables/useMember.ts +107 -0
  49. package/composables/useOrder.ts +22 -0
  50. package/composables/useOrg.ts +106 -0
  51. package/composables/useOrgPermission.ts +27 -0
  52. package/composables/usePayment.ts +22 -0
  53. package/composables/usePaymentMethod.ts +347 -0
  54. package/composables/usePermission.ts +54 -0
  55. package/composables/usePrice.ts +15 -0
  56. package/composables/usePromoCode.ts +43 -0
  57. package/composables/useRecapPermission.ts +26 -0
  58. package/composables/useRole.ts +89 -0
  59. package/composables/useSchoolPermission.ts +13 -0
  60. package/composables/useSubscription.ts +264 -0
  61. package/composables/useUser.ts +102 -0
  62. package/composables/useUtils.ts +294 -0
  63. package/composables/useVerification.ts +19 -0
  64. package/error.vue +41 -0
  65. package/eslint.config.js +3 -0
  66. package/layouts/plain.vue +7 -0
  67. package/middleware/01.auth.ts +14 -0
  68. package/middleware/org.ts +16 -0
  69. package/nuxt.config.ts +48 -0
  70. package/package.json +35 -0
  71. package/pages/index.vue +3 -0
  72. package/pages/payment-method-cancel-link.vue +31 -0
  73. package/pages/payment-method-failed-link.vue +31 -0
  74. package/pages/payment-method-linked.vue +31 -0
  75. package/pages/require-organization-membership.vue +47 -0
  76. package/pages/unauthorized.vue +29 -0
  77. package/plugins/API.ts +58 -0
  78. package/plugins/iconify.client.ts +5 -0
  79. package/plugins/vuetify.ts +55 -0
  80. package/public/bdo-logo.svg +4 -0
  81. package/public/bpi-logo.svg +74 -0
  82. package/public/chinabank-logo.svg +120 -0
  83. package/public/gcash-logo.png +0 -0
  84. package/public/gcash-logo.svg +65 -0
  85. package/public/grabpay-logo.svg +99 -0
  86. package/public/paymaya-logo.jpg +0 -0
  87. package/public/paymaya-logo.png +0 -0
  88. package/public/paymaya-logo.svg +25 -0
  89. package/public/qrph-c567ff0f-ab6d-4662-86bf-24c6c731d8a8-logo.svg +20 -0
  90. package/public/rcbc-logo.svg +15 -0
  91. package/public/shopeepay-logo.svg +89 -0
  92. package/public/ubp-logo.svg +88 -0
  93. package/tsconfig.json +3 -0
  94. package/types/address.d.ts +13 -0
  95. package/types/invoice.d.ts +28 -0
  96. package/types/local.d.ts +25 -0
  97. package/types/member.d.ts +12 -0
  98. package/types/org.d.ts +13 -0
  99. package/types/payment-method.d.ts +11 -0
  100. package/types/payment.d.ts +18 -0
  101. package/types/permission.d.ts +14 -0
  102. package/types/price.d.ts +17 -0
  103. package/types/promo-code.d.ts +19 -0
  104. package/types/role.d.ts +13 -0
  105. package/types/subscription.d.ts +29 -0
  106. package/types/user.d.ts +21 -0
  107. package/types/verification.d.ts +15 -0
  108. package/types/xendit.d.ts +3 -0
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <nuxt-link
3
+ class="text-h6 font-weight-medium text-decoration-none"
4
+ style="color: unset"
5
+ :to="{ name: 'index' }"
6
+ >
7
+ GoWeekdays
8
+ </nuxt-link>
9
+ </template>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <v-list-item :class="props.divider ? border : defaultBorder">
3
+ <v-row>
4
+ <v-col cols="4" class="text-subtitle-2">
5
+ <slot name="title"> List Title </slot>
6
+ </v-col>
7
+
8
+ <v-col cols="8" class="text-subtitle-2">
9
+ <slot name="value"> value </slot>
10
+ </v-col>
11
+ </v-row>
12
+
13
+ <template #append>
14
+ <slot name="append">
15
+ <v-icon size="30">{{ props.icon }}</v-icon>
16
+ </slot>
17
+ </template>
18
+ </v-list-item>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ const props = defineProps({
23
+ divider: {
24
+ type: Boolean,
25
+ default: true,
26
+ },
27
+ icon: {
28
+ type: String,
29
+ default: "mdi-chevron-right",
30
+ },
31
+ });
32
+
33
+ const defaultBorder = "pa-0 pl-8 pr-6 py-4";
34
+ const border = "border-b-sm" + " " + defaultBorder;
35
+ </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="arrow-navigation">
3
+ <v-btn
4
+ icon="mdi-chevron-left"
5
+ variant="text"
6
+ density="comfortable"
7
+ :disabled="page <= 1"
8
+ @click="decrement"
9
+ />
10
+ <v-btn
11
+ icon="mdi-chevron-right"
12
+ variant="text"
13
+ density="comfortable"
14
+ :disabled="page >= props.length"
15
+ @click="increment"
16
+ />
17
+ </div>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ const page = defineModel({ type: Number, default: 0 });
22
+ function increment() {
23
+ page.value++;
24
+ }
25
+
26
+ function decrement() {
27
+ page.value--;
28
+ }
29
+ const emit = defineEmits(["update:value"]);
30
+ watch(page, () => {
31
+ emit("update:value", page.value);
32
+ });
33
+
34
+ const props = defineProps({
35
+ length: {
36
+ type: Number,
37
+ required: true,
38
+ default: 0,
39
+ },
40
+ });
41
+ </script>
@@ -0,0 +1,452 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col v-if="props.seatManagement !== 'index'" cols="12" class="mb-2">
4
+ <v-row no-gutters>
5
+ <v-btn
6
+ class="text-none"
7
+ rounded="pill"
8
+ variant="tonal"
9
+ :to="{
10
+ name: props.seatManagement,
11
+ params: { organization: props.orgId },
12
+ }"
13
+ size="large"
14
+ >
15
+ Manage seats
16
+ </v-btn>
17
+ </v-row>
18
+ </v-col>
19
+
20
+ <v-col cols="12">
21
+ <v-card width="100%" variant="outlined" border="thin" rounded="lg">
22
+ <v-toolbar density="compact" color="grey-lighten-4">
23
+ <template #prepend>
24
+ <v-btn fab icon density="comfortable" @click="getAll()">
25
+ <v-icon>mdi-refresh</v-icon>
26
+ </v-btn>
27
+ </template>
28
+
29
+ <template #append>
30
+ <v-row no-gutters justify="end" align="center">
31
+ <span class="mr-2 text-caption text-font gray">
32
+ {{ pageRange }}
33
+ </span>
34
+ <local-pagination
35
+ v-model="page"
36
+ :length="pages"
37
+ @update:value="getAll()"
38
+ />
39
+ </v-row>
40
+ </template>
41
+
42
+ <template #extension>
43
+ <v-tabs>
44
+ <v-tab
45
+ v-for="tab in [
46
+ { name: 'Active', status: 'active' },
47
+ { name: 'Suspended', status: 'suspended' },
48
+ ]"
49
+ :key="tab.status"
50
+ :to="{
51
+ name: props.route,
52
+ params: setRouteParams({
53
+ status: tab.status,
54
+ organization: props.orgId,
55
+ }),
56
+ }"
57
+ >
58
+ {{ tab.name }}
59
+ </v-tab>
60
+ </v-tabs>
61
+ </template>
62
+ </v-toolbar>
63
+
64
+ <v-data-table
65
+ :headers="props.headers"
66
+ :items="items"
67
+ item-value="_id"
68
+ items-per-page="20"
69
+ fixed-header
70
+ hide-default-footer
71
+ style="max-height: calc(100vh - (126px))"
72
+ :loading="loading"
73
+ >
74
+ <template #item.nature="{ item }">
75
+ {{ replaceMatch(item.nature, "_", " ") }}
76
+ </template>
77
+ <template #item.action-table="{ item }">
78
+ <v-menu
79
+ v-if="
80
+ (status === 'active' &&
81
+ (canSuspendMembers || canDeleteMembers)) ||
82
+ (status === 'suspended' &&
83
+ (canActivateMembers || canDeleteMembers))
84
+ "
85
+ v-model="item.menuOpen"
86
+ offset-y
87
+ width="150"
88
+ >
89
+ <template v-slot:activator="{ props }">
90
+ <v-icon v-bind="props">mdi-dots-horizontal</v-icon>
91
+ </template>
92
+ <v-list>
93
+ <v-list-item
94
+ v-if="props.assignRole"
95
+ @click="
96
+ setMember({
97
+ dialog: true,
98
+ mode: 'assign-role',
99
+ role: item,
100
+ })
101
+ "
102
+ >
103
+ Assign Role
104
+ </v-list-item>
105
+
106
+ <v-list-item
107
+ v-if="status === 'active' && canSuspendMembers"
108
+ @click="openUpdateDialog(item.user, 'suspended')"
109
+ >
110
+ Suspend
111
+ </v-list-item>
112
+ <v-list-item
113
+ v-if="status === 'suspended' && canActivateMembers"
114
+ @click="openUpdateDialog(item.user, 'active')"
115
+ >
116
+ Activate
117
+ </v-list-item>
118
+ <v-list-item
119
+ v-if="canDeleteMembers"
120
+ @click="openUpdateDialog(item.user, 'deleted')"
121
+ >
122
+ Delete
123
+ </v-list-item>
124
+ </v-list>
125
+ </v-menu>
126
+ </template>
127
+ </v-data-table>
128
+ </v-card>
129
+ </v-col>
130
+ <ConfirmDialog
131
+ v-model="confirmDialog"
132
+ :loading="updateLoading"
133
+ @submit="handleUpdateMemberStatus"
134
+ >
135
+ <template #title>
136
+ <span class="font-weight-medium text-h5">
137
+ {{ updateActionText }} Member</span
138
+ >
139
+ </template>
140
+
141
+ <template #description>
142
+ <p class="text-subtitle-2">
143
+ Are you sure you want to {{ updateActionText }} this Member? This
144
+ action cannot be undone.
145
+ </p>
146
+ </template>
147
+
148
+ <template #footer>
149
+ <v-btn
150
+ variant="text"
151
+ @click="confirmDialog = false"
152
+ :disabled="updateLoading"
153
+ >
154
+ Close
155
+ </v-btn>
156
+ <v-btn
157
+ color="primary"
158
+ variant="flat"
159
+ @click="handleUpdateMemberStatus"
160
+ :loading="updateLoading"
161
+ >
162
+ {{ updateActionText }} Member
163
+ </v-btn>
164
+ </template>
165
+ </ConfirmDialog>
166
+
167
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
168
+
169
+ <v-dialog v-model="assignRoleDialog" max-width="400px">
170
+ <v-card>
171
+ <v-card-title class="text-h5">Assign Role</v-card-title>
172
+ <v-card-text>
173
+ <v-form v-model="memberRoleForm" @submit.prevent="updateMemberRole()">
174
+ <v-row no-gutters>
175
+ <v-col cols="12" class="mb-2">
176
+ <v-row no-gutters>
177
+ <InputLabel class="text-capitalize" title="Name" required />
178
+ <v-col cols="12">
179
+ <v-select
180
+ v-model="selectedRole"
181
+ :items="roles"
182
+ item-value="_id"
183
+ item-title="name"
184
+ density="comfortable"
185
+ :rules="[requiredRule]"
186
+ :error-messages="
187
+ selectedRole === selectedMemberRole
188
+ ? 'Role already assigned'
189
+ : ''
190
+ "
191
+ persistent-hint
192
+ ></v-select>
193
+ </v-col>
194
+
195
+ <v-col cols="12" class="text-error text-center">
196
+ {{ message }}
197
+ </v-col>
198
+ </v-row>
199
+ </v-col>
200
+
201
+ <v-col cols="12">
202
+ <v-row>
203
+ <v-col cols="6">
204
+ <v-btn
205
+ block
206
+ variant="text"
207
+ @click="setMember({ mode: 'assign-role' })"
208
+ >
209
+ Cancel
210
+ </v-btn>
211
+ </v-col>
212
+ <v-col cols="6">
213
+ <v-btn
214
+ block
215
+ variant="flat"
216
+ color="black"
217
+ :disabled="!memberRoleForm"
218
+ type="submit"
219
+ >
220
+ Submit
221
+ </v-btn>
222
+ </v-col>
223
+ </v-row>
224
+ </v-col>
225
+ </v-row>
226
+ </v-form>
227
+ </v-card-text>
228
+ </v-card>
229
+ </v-dialog>
230
+ </v-row>
231
+ </template>
232
+
233
+ <script setup lang="ts">
234
+ const props = defineProps({
235
+ orgId: {
236
+ type: String,
237
+ default: "",
238
+ },
239
+ customerId: {
240
+ type: String,
241
+ default: "",
242
+ },
243
+ siteId: {
244
+ type: String,
245
+ default: "",
246
+ },
247
+ status: {
248
+ type: String,
249
+ default: "active",
250
+ },
251
+ type: {
252
+ type: String,
253
+ default: "organization",
254
+ },
255
+ seatManagement: {
256
+ type: String,
257
+ default: "index",
258
+ },
259
+ inviteMember: {
260
+ type: String,
261
+ default: "index",
262
+ },
263
+ route: {
264
+ type: String,
265
+ default: "index",
266
+ },
267
+ canActivateMembers: {
268
+ type: Boolean,
269
+ default: false,
270
+ },
271
+ canSuspendMembers: {
272
+ type: Boolean,
273
+ default: false,
274
+ },
275
+ canDeleteMembers: {
276
+ type: Boolean,
277
+ default: false,
278
+ },
279
+ assignRole: {
280
+ type: Boolean,
281
+ default: false,
282
+ },
283
+ headers: {
284
+ type: Array as PropType<Array<Record<string, string>>>,
285
+ default: () => [
286
+ {
287
+ title: "Name",
288
+
289
+ value: "name",
290
+ },
291
+ {
292
+ title: "Role",
293
+
294
+ value: "roleName",
295
+ },
296
+ {
297
+ title: "Organization",
298
+
299
+ value: "orgName",
300
+ },
301
+ {
302
+ title: "Action",
303
+ value: "action-table",
304
+ },
305
+ ],
306
+ },
307
+ });
308
+
309
+ const items = ref<Array<Record<string, any>>>([]);
310
+ const page = ref(1);
311
+ const pages = ref(10);
312
+ const pageRange = ref("-- - -- of --");
313
+
314
+ const message = ref("");
315
+ const messageSnackbar = ref(false);
316
+ const messageColor = ref("");
317
+
318
+ const {
319
+ getAll: _getAll,
320
+ updateMemberStatus: _updateMemberStatus,
321
+ updateMemberRole: _updateMemberRole,
322
+ } = useMember();
323
+
324
+ const { headerSearch } = useLocal();
325
+ const { replaceMatch, setRouteParams, requiredRule } = useUtils();
326
+
327
+ const {
328
+ data: getAllReq,
329
+ refresh: getAll,
330
+ status: getAllReqStatus,
331
+ } = useLazyAsyncData("get-all-members-by-status" + props.status, () =>
332
+ _getAll({
333
+ status: props.status,
334
+ org: props.orgId,
335
+ search: headerSearch.value,
336
+ page: page.value,
337
+ type: props.type,
338
+ })
339
+ );
340
+
341
+ const loading = computed(() => getAllReqStatus.value === "pending");
342
+
343
+ watchEffect(() => {
344
+ if (getAllReq.value) {
345
+ items.value = getAllReq.value.items;
346
+ pages.value = getAllReq.value.pages;
347
+ pageRange.value = getAllReq.value.pageRange;
348
+ }
349
+ });
350
+
351
+ watch(headerSearch, () => {
352
+ getAll();
353
+ });
354
+
355
+ const confirmDialog = ref(false);
356
+ const selectedMemberId = ref<string | null>(null);
357
+ const updateLoading = ref(false);
358
+ const updateAction = ref("");
359
+
360
+ const memberRoleForm = ref(false);
361
+ const assignRoleDialog = ref(false);
362
+ const selectedRole = ref<string | null>(null);
363
+ const selectedMemberRole = ref("");
364
+
365
+ function setMember({
366
+ dialog = false,
367
+ mode = "",
368
+ role = {} as Record<string, any>,
369
+ } = {}) {
370
+ if (mode === "assign-role") {
371
+ assignRoleDialog.value = dialog;
372
+ }
373
+
374
+ selectedMemberId.value = role._id;
375
+ selectedRole.value = role.role;
376
+ selectedMemberRole.value = role.role;
377
+ }
378
+
379
+ const roles = ref<Array<Record<string, any>>>([]);
380
+ const { getRoles } = useRole();
381
+ const { data: getAllRoleReq } = useLazyAsyncData("get-roles", () =>
382
+ getRoles({ org: props.orgId, type: props.type, limit: 20 })
383
+ );
384
+
385
+ watchEffect(() => {
386
+ if (getAllRoleReq.value) {
387
+ roles.value = getAllRoleReq.value.items;
388
+ }
389
+ });
390
+
391
+ watchEffect(() => {
392
+ if (selectedRole.value) {
393
+ message.value = "";
394
+ }
395
+ });
396
+
397
+ async function updateMemberRole() {
398
+ try {
399
+ await _updateMemberRole(
400
+ selectedMemberId.value ?? "",
401
+ selectedRole.value ?? "",
402
+ props.type,
403
+ props.orgId
404
+ );
405
+ await setMember({ mode: "assign-role" });
406
+ await getAll();
407
+ } catch (error: any) {
408
+ message.value = error?.response?._data?.message || "Failed to update role";
409
+ }
410
+ }
411
+
412
+ function openUpdateDialog(id: string, action: string) {
413
+ updateAction.value = action;
414
+ selectedMemberId.value = id;
415
+ confirmDialog.value = true;
416
+ }
417
+
418
+ function showMessage(msg: string, color: string) {
419
+ message.value = msg;
420
+ messageColor.value = color;
421
+ messageSnackbar.value = true;
422
+ }
423
+
424
+ async function handleUpdateMemberStatus() {
425
+ if (!selectedMemberId.value) return;
426
+ updateLoading.value = true;
427
+ try {
428
+ const res = await _updateMemberStatus(
429
+ selectedMemberId.value,
430
+ updateAction.value
431
+ );
432
+
433
+ confirmDialog.value = false;
434
+ showMessage(res.message, "success");
435
+ getAll();
436
+ } catch (error: any) {
437
+ const errorMessage = error?.response?._data?.message;
438
+ showMessage(errorMessage, "error");
439
+ } finally {
440
+ updateLoading.value = false;
441
+ selectedMemberId.value = null;
442
+ }
443
+ }
444
+ const updateActionText = computed(() => {
445
+ const map: Record<string, string> = {
446
+ active: "Activate",
447
+ suspended: "Suspend",
448
+ deleted: "Delete",
449
+ };
450
+ return map[updateAction.value] || updateAction.value;
451
+ });
452
+ </script>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <v-list-group v-if="children && children.length">
3
+ <template #activator="{ props }">
4
+ <v-list-item
5
+ v-bind="props"
6
+ :prepend-icon="icon"
7
+ class="text-subtitle-2 font-weight-medium px-3"
8
+ :rounded="prop.rounded"
9
+ >
10
+ {{ title }}
11
+ </v-list-item>
12
+ </template>
13
+
14
+ <NavigationItem
15
+ v-for="(child, childIndex) in children"
16
+ :key="`${child.title}-${childIndex}`"
17
+ :title="child.title"
18
+ :icon="child.icon"
19
+ :route="child.route"
20
+ :children="child.children"
21
+ :disabled="child.disabled"
22
+ :rounded="prop.rounded"
23
+ />
24
+ </v-list-group>
25
+
26
+ <v-list-item
27
+ v-else
28
+ :prepend-icon="icon"
29
+ :to="prop.route"
30
+ :rounded="prop.rounded"
31
+ class="text-subtitle-2 font-weight-medium px-3"
32
+ :disabled="prop.disabled"
33
+ >
34
+ {{ title }}
35
+ </v-list-item>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ const prop = defineProps({
40
+ title: {
41
+ type: String,
42
+ required: true,
43
+ default: "Title",
44
+ },
45
+ icon: {
46
+ type: String,
47
+ required: false,
48
+ default: "",
49
+ },
50
+ route: {
51
+ type: Object,
52
+ default() {
53
+ return { name: "", params: {} };
54
+ },
55
+ },
56
+ children: {
57
+ type: Array<TNavigationItem>,
58
+ required: false,
59
+ default: () => [],
60
+ },
61
+ disabled: {
62
+ type: Boolean,
63
+ required: false,
64
+ default: false,
65
+ },
66
+
67
+ rounded: {
68
+ type: String,
69
+ required: false,
70
+ default: "e-pill",
71
+ },
72
+ });
73
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <div class="maintenance-container">
3
+ <h1>🚧 Page Under Maintenance</h1>
4
+ <p>
5
+ We're currently working on improving this page to serve you better. Please
6
+ check back soon!
7
+ </p>
8
+ <p>Thank you for your patience.</p>
9
+ </div>
10
+ </template>
11
+
12
+ <style scoped>
13
+ .maintenance-container {
14
+ display: flex;
15
+ flex-direction: column;
16
+ justify-content: center;
17
+ align-items: center;
18
+ text-align: center;
19
+ min-height: 50vh;
20
+ margin: 0;
21
+ font-family: Arial, sans-serif;
22
+ color: #333;
23
+ }
24
+
25
+ h1 {
26
+ font-size: 2rem;
27
+ margin-bottom: 1rem;
28
+ }
29
+
30
+ p {
31
+ font-size: 1.1rem;
32
+ line-height: 1.5;
33
+ }
34
+ </style>