@saooti/octopus-sdk 41.12.0-beta2 → 41.12.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.
- package/.claude/scheduled_tasks.lock +1 -0
- package/CHANGELOG.md +7 -1
- package/index.ts +1 -1
- package/package.json +3 -3
- package/src/api/index.ts +1 -0
- package/src/api/radioApi.ts +18 -2
- package/src/api/rubriquesApi.ts +68 -0
- package/src/components/composable/form/useOctopusDropdown.ts +5 -2
- package/src/components/composable/usePresentationItem.ts +78 -0
- package/src/components/display/emission/EmissionInlineList.vue +1 -1
- package/src/components/display/emission/EmissionPresentationItem.vue +25 -16
- package/src/components/display/podcastmaker/PodcastmakerHeader.vue +60 -45
- package/src/components/display/podcasts/PodcastPresentationList.vue +103 -89
- package/src/components/form/ClassicButtonGroup.vue +12 -6
- package/src/components/form/OctopusMultiselect.vue +86 -9
- package/src/components/form/OctopusSelect.vue +29 -2
- package/src/components/layouts/PresentationItem.vue +149 -104
- package/src/locale/de.json +1 -0
- package/src/locale/en.json +1 -0
- package/src/locale/es.json +1 -0
- package/src/locale/fr.json +1 -0
- package/src/locale/it.json +1 -0
- package/src/locale/sl.json +1 -0
- package/src/stores/CacheStore.ts +17 -11
- package/src/stores/ParamSdkStore.ts +22 -0
- package/src/stores/class/general/organisation.ts +10 -0
- package/src/stores/class/radio/canal.ts +30 -8
- package/src/stores/class/radio/recurrence.ts +6 -5
- package/src/style/_variables.scss +2 -0
- package/src/style/bootstrap.scss +3 -2
- package/tests/components/composable/usePresentationItem.spec.ts +163 -0
- package/tests/components/display/emission/EmissionPresentationItem.spec.ts +45 -0
- package/tests/components/display/podcasts/PodcastPresentationList.spec.ts +107 -0
- package/tests/components/form/ClassicButtonGroup.spec.ts +26 -0
- package/tests/components/form/OctopusMultiselect.spec.ts +298 -0
- package/tests/components/form/OctopusSelect.spec.ts +71 -0
- package/tests/components/layouts/PresentationItem.spec.ts +36 -0
- package/tests/stores/CacheStore.spec.ts +109 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"92d5c13f-8d38-4dc6-a66d-c7256640dc6f","pid":10553,"procStart":"32347","acquiredAt":1783406560303}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
## 41.12.0 (
|
|
3
|
+
## 41.12.0 (13/07/2026)
|
|
4
4
|
|
|
5
5
|
**Features**
|
|
6
6
|
|
|
@@ -15,15 +15,21 @@
|
|
|
15
15
|
- Mise en commun de code entre `OctopusSelect` & `OctopusMultiselect`
|
|
16
16
|
- Ajustement de `OctopusMultiselect` pour fonctionner correctement avec des
|
|
17
17
|
objets identiques avec références différentes
|
|
18
|
+
- Ajout de nouvelles options à `OctopusMultiselect` pour permettre la saisie
|
|
19
|
+
de données et avoir une meilleure UX en cas de longue liste
|
|
18
20
|
- Ajout de nouvelles classes utilitaires dans le css
|
|
19
21
|
- Ajout de nouvelles options d'apparence pour `ClassicButton`
|
|
20
22
|
- Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
|
|
21
23
|
- Ajout de variables CSS pour configurer l'aspect de divers boutons
|
|
24
|
+
- Ajout d'une propriété SDK permettant d'afficher des tags sur les éléments de
|
|
25
|
+
type `PresentationItem`
|
|
26
|
+
- Ajout de l'api `rubriqueApi`
|
|
22
27
|
|
|
23
28
|
**Fixes**
|
|
24
29
|
|
|
25
30
|
- Correction d'une exception dans `deepEquals` si la comparaison est faite entre
|
|
26
31
|
une variable primitive et un objet
|
|
32
|
+
- Correction du placement du header `PodcastmakerHeader`
|
|
27
33
|
|
|
28
34
|
**Misc**
|
|
29
35
|
|
package/index.ts
CHANGED
|
@@ -194,7 +194,7 @@ export {
|
|
|
194
194
|
type ProviderTts,
|
|
195
195
|
type Voice
|
|
196
196
|
} from "./src/stores/class/transcript/transcriptParams.ts";
|
|
197
|
-
export
|
|
197
|
+
export * from "./src/stores/class/radio/canal";
|
|
198
198
|
export { type Cartouchier } from "./src/stores/class/cartouchier/cartouchier.ts";
|
|
199
199
|
export { type Cartouche, emptyCartouche } from "./src/stores/class/cartouchier/cartouche.ts";
|
|
200
200
|
export { type PlaylistMedia } from "./src/stores/class/radio/playlistMedia";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saooti/octopus-sdk",
|
|
3
|
-
"version": "41.12.0
|
|
3
|
+
"version": "41.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Javascript SDK for using octopus",
|
|
6
6
|
"author": "Saooti",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"typescript-eslint": "^8.47.0",
|
|
52
52
|
"video.js": "^8.23.6",
|
|
53
53
|
"videojs-quality-selector-hls": "^1.1.1",
|
|
54
|
-
"vite": "^
|
|
54
|
+
"vite": "^8.0.16",
|
|
55
55
|
"vite-bundle-visualizer": "^1.2.1",
|
|
56
56
|
"vue-material-design-icons": "^5.3.1",
|
|
57
57
|
"vue-recaptcha": "^2.0.3",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@stylistic/eslint-plugin": "*",
|
|
65
65
|
"@types/sockjs-client": "^1.5.4",
|
|
66
66
|
"@types/webpack-env": "^1.18.8",
|
|
67
|
-
"@vitejs/plugin-vue": "^
|
|
67
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
68
68
|
"@vue/test-utils": "^2.4.6",
|
|
69
69
|
"eslint": "^9.39.1",
|
|
70
70
|
"eslint-plugin-vue": "10.9.1",
|
package/src/api/index.ts
CHANGED
package/src/api/radioApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Canal } from "../stores/class/radio/canal";
|
|
1
|
+
import { Ambiance, Canal } from "../stores/class/radio/canal";
|
|
2
2
|
import classicApi from "./classicApi";
|
|
3
3
|
import { ModuleApi } from "./apiConnection";
|
|
4
4
|
|
|
@@ -26,7 +26,23 @@ async function getAll(organisationId: string): Promise<Array<Canal>> {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Set the default ambiance for the given canal
|
|
31
|
+
* @param canalId ID of the canal to change
|
|
32
|
+
* @param ambiance New ambiance settings
|
|
33
|
+
* @returns The modified ambiance
|
|
34
|
+
*/
|
|
35
|
+
async function setDefaultAmbiance(canalId: number, ambiance: Ambiance): Promise<Ambiance> {
|
|
36
|
+
return await classicApi.putData<Ambiance>({
|
|
37
|
+
api: ModuleApi.RADIO,
|
|
38
|
+
path: `ambiance/canal/${canalId}/default`,
|
|
39
|
+
dataToSend: ambiance
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export const radioApi = {
|
|
30
45
|
get,
|
|
31
|
-
getAll
|
|
46
|
+
getAll,
|
|
47
|
+
setDefaultAmbiance
|
|
32
48
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { classicApi, ModuleApi } from "@saooti/octopus-sdk";
|
|
2
|
+
import { Rubrique } from "@/stores/class/rubrique/rubrique";
|
|
3
|
+
import { Rubriquage } from "@/stores/class/rubrique/rubriquage";
|
|
4
|
+
import { useCacheStore } from "../stores/CacheStore";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find rubriquages according to criterias
|
|
8
|
+
* @param organisationIds List of organisation IDs for which to retrieve the
|
|
9
|
+
* rubriquage
|
|
10
|
+
* @param searchOptions Additional search options
|
|
11
|
+
*/
|
|
12
|
+
async function searchRubriquages(organisationIds: Array<string>, searchOptions?: {
|
|
13
|
+
rubriquageId?: number;
|
|
14
|
+
organisationId?: string|Array<string>;
|
|
15
|
+
query?: string;
|
|
16
|
+
}): Promise<Array<Rubriquage>> {
|
|
17
|
+
return classicApi.fetchData<Array<Rubriquage>>({
|
|
18
|
+
api: ModuleApi.DEFAULT,
|
|
19
|
+
path: 'rubriquage/find',
|
|
20
|
+
parameters: {
|
|
21
|
+
organisationId: organisationIds,
|
|
22
|
+
...searchOptions
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function searchRubriques(searchOptions?: {
|
|
28
|
+
rubriquageId?: number;
|
|
29
|
+
organisationId?: string|Array<string>;
|
|
30
|
+
query?: string;
|
|
31
|
+
}): Promise<Array<Rubrique>> {
|
|
32
|
+
return classicApi.fetchData<Array<Rubrique>>({
|
|
33
|
+
api: ModuleApi.DEFAULT,
|
|
34
|
+
path: 'rubrique/search',
|
|
35
|
+
parameters: searchOptions
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch rubrique data by ID
|
|
41
|
+
* @see getCachedRubrique
|
|
42
|
+
* @param rubriqueId ID of the rubrique to fetch
|
|
43
|
+
* @returns The rubrique
|
|
44
|
+
*/
|
|
45
|
+
async function getRubrique(rubriqueId: number): Promise<Rubrique> {
|
|
46
|
+
return classicApi.fetchData<Rubrique>({
|
|
47
|
+
api: ModuleApi.DEFAULT,
|
|
48
|
+
path: `rubrique/${rubriqueId}`
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch rubrique data by ID
|
|
54
|
+
* If the rubrique is available in the cache, return it from the cache
|
|
55
|
+
* @param rubriqueId ID of the rubrique to fetch
|
|
56
|
+
* @returns The rubrique
|
|
57
|
+
*/
|
|
58
|
+
async function getCachedRubrique(rubriqueId: number): Promise<Rubrique> {
|
|
59
|
+
const cacheStore = useCacheStore();
|
|
60
|
+
return cacheStore.getData(`rubrique-${rubriqueId}`, () => getRubrique(rubriqueId));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const rubriquesApi = {
|
|
64
|
+
getRubrique,
|
|
65
|
+
getCachedRubrique,
|
|
66
|
+
searchRubriquages,
|
|
67
|
+
searchRubriques
|
|
68
|
+
};
|
|
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
|
|
|
4
4
|
|
|
5
5
|
export interface OctopusDropdownProps<T> {
|
|
6
6
|
options: T[];
|
|
7
|
-
optionLabel
|
|
7
|
+
optionLabel?: keyof T & string;
|
|
8
8
|
optionKey?: keyof T;
|
|
9
9
|
isDisabled?: boolean;
|
|
10
10
|
placeholder?: string;
|
|
@@ -51,7 +51,10 @@ export function useOctopusDropdown<T>(
|
|
|
51
51
|
const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
|
|
52
52
|
|
|
53
53
|
function getLabel(option: T): string {
|
|
54
|
-
|
|
54
|
+
if (props.optionLabel) {
|
|
55
|
+
return option[props.optionLabel] as string;
|
|
56
|
+
}
|
|
57
|
+
return option as unknown as string;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function openDropdown(): void {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Emission } from "@/stores/class/general/emission";
|
|
2
|
+
import { state } from "../../stores/ParamSdkStore";
|
|
3
|
+
import { useGeneralStore } from "../../stores/GeneralStore";
|
|
4
|
+
import { Podcast } from "../../stores/class/general/podcast";
|
|
5
|
+
import { rubriquesApi } from "../../api";
|
|
6
|
+
import { useDayjs } from "./useDayjs";
|
|
7
|
+
|
|
8
|
+
export const usePresentationItem = () => {
|
|
9
|
+
const generalStore = useGeneralStore();
|
|
10
|
+
const { formatDate } = useDayjs();
|
|
11
|
+
|
|
12
|
+
function iabTags(emission: Emission): Array<string> {
|
|
13
|
+
return generalStore.storedCategories
|
|
14
|
+
.filter(cat => emission.iabIds.includes(cat.id))
|
|
15
|
+
.map(cat => cat.name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function rubriqueTags(element: Podcast|Emission): Promise<Array<string>> {
|
|
19
|
+
const rubriqueIds: Array<number> = [];
|
|
20
|
+
if (element.rubriqueIds) {
|
|
21
|
+
rubriqueIds.push(...element.rubriqueIds);
|
|
22
|
+
}
|
|
23
|
+
if ('emission' in element && element.emission.rubriqueIds) {
|
|
24
|
+
rubriqueIds.push(...element.emission.rubriqueIds);
|
|
25
|
+
}
|
|
26
|
+
const promises = rubriqueIds.map(rubriquesApi.getCachedRubrique);
|
|
27
|
+
const rubriques = await Promise.all(promises);
|
|
28
|
+
|
|
29
|
+
if (state.presentationItems.tagsRubriquageId) {
|
|
30
|
+
const rubriquageId = state.presentationItems.tagsRubriquageId;
|
|
31
|
+
return rubriques.filter(r => r.rubriquageId === rubriquageId).map(r => r.name);
|
|
32
|
+
} else {
|
|
33
|
+
return rubriques.map(r => r.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function tagsFor(element: Podcast|Emission): Promise<Array<string>|undefined> {
|
|
38
|
+
const type = state.presentationItems.tags;
|
|
39
|
+
let tags: Array<string>|undefined = undefined;
|
|
40
|
+
if (type === 'iab') {
|
|
41
|
+
const emission = 'emission' in element ? element.emission : element;
|
|
42
|
+
tags = iabTags(emission);
|
|
43
|
+
} else if (type === 'rubrique') {
|
|
44
|
+
tags = await rubriqueTags(element);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (tags && state.presentationItems.tagsLimit) {
|
|
48
|
+
return tags.slice(0, state.presentationItems.tagsLimit);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return tags;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function additionalInfoFor(element: Podcast|Emission): Array<string>|undefined {
|
|
55
|
+
const prop = state.presentationItems.additionalInfo;
|
|
56
|
+
if (!prop?.length) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const podcast = 'emission' in element ? element : null;
|
|
61
|
+
const emission = podcast?.emission ?? element as Emission;
|
|
62
|
+
|
|
63
|
+
return prop.map(property => {
|
|
64
|
+
if (property === 'date') {
|
|
65
|
+
if (podcast?.pubDate) {
|
|
66
|
+
return formatDate(podcast.pubDate);
|
|
67
|
+
} else {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
} else if (property === 'productor') {
|
|
71
|
+
return emission.orga.name;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}).filter(p => p !== null);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { tagsFor, additionalInfoFor };
|
|
78
|
+
};
|
|
@@ -2,33 +2,42 @@
|
|
|
2
2
|
Component to display an emission with its description
|
|
3
3
|
-->
|
|
4
4
|
<template>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
<PresentationItem
|
|
6
|
+
:name="emission.name"
|
|
7
|
+
:route="route"
|
|
8
|
+
:image-url="emission.imageUrl"
|
|
9
|
+
:description="isDescription ? emission.description : undefined"
|
|
10
|
+
:vertical="isVertical"
|
|
11
|
+
:tags="tags"
|
|
12
|
+
:additional-info="additionalInfoFor(emission)"
|
|
13
|
+
/>
|
|
12
14
|
</template>
|
|
13
15
|
|
|
14
16
|
<script setup lang="ts">
|
|
15
17
|
import { Emission } from "@/stores/class/general/emission";
|
|
16
18
|
import PresentationItem from "../../layouts/PresentationItem.vue";
|
|
17
|
-
import { computed } from "vue";
|
|
19
|
+
import { computed, ref, watch } from "vue";
|
|
18
20
|
import { RouteLocationRaw } from "vue-router";
|
|
21
|
+
import { usePresentationItem } from "../../composable/usePresentationItem";
|
|
19
22
|
|
|
20
23
|
//Props
|
|
21
24
|
const props = defineProps<{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/** The emission to display */
|
|
26
|
+
emission: Emission;
|
|
27
|
+
/** When true display the card vertically */
|
|
28
|
+
isVertical?: boolean;
|
|
29
|
+
/** When true also display the description */
|
|
30
|
+
isDescription?: boolean;
|
|
28
31
|
}>();
|
|
29
32
|
|
|
33
|
+
const tags = ref([]);
|
|
34
|
+
const { tagsFor, additionalInfoFor } = usePresentationItem();
|
|
35
|
+
|
|
36
|
+
watch(props.emission, async () => {
|
|
37
|
+
tags.value = await tagsFor(props.emission);
|
|
38
|
+
}, { immediate: true });
|
|
39
|
+
|
|
30
40
|
const route = computed((): RouteLocationRaw => {
|
|
31
|
-
|
|
41
|
+
return { name: 'emission', params: { emissionId: props.emission.emissionId } };
|
|
32
42
|
});
|
|
33
|
-
|
|
34
43
|
</script>
|
|
@@ -1,72 +1,87 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<div class="page-element-title-container">
|
|
3
|
+
<div class="page-element-title">
|
|
4
|
+
<h1>{{ pageTitle }}</h1>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="page-element-bg" :style="backgroundDisplay" />
|
|
5
7
|
</div>
|
|
6
|
-
<div class="page-element-bg" :style="backgroundDisplay" />
|
|
7
|
-
</div>
|
|
8
8
|
</template>
|
|
9
9
|
|
|
10
10
|
<script setup lang="ts">
|
|
11
|
-
import { computed } from "vue";
|
|
12
|
-
import {useImageProxy} from "../../composable/useImageProxy";
|
|
11
|
+
import { computed, onMounted } from "vue";
|
|
12
|
+
import { useImageProxy } from "../../composable/useImageProxy";
|
|
13
13
|
|
|
14
14
|
//Props
|
|
15
15
|
const props = defineProps({
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
pageTitle: { default: undefined, type: String },
|
|
17
|
+
imgUrl: { default: undefined, type: String },
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
+
onMounted(() => {
|
|
21
|
+
const elt = document.querySelector('.page-element-title-container');
|
|
22
|
+
|
|
23
|
+
// Retrieve header
|
|
24
|
+
const header = document.getElementsByTagName('header');
|
|
25
|
+
if (header.length) {
|
|
26
|
+
// Get header height
|
|
27
|
+
const height = (header[0] as HTMLElement).offsetHeight;
|
|
28
|
+
// Update PM header position according to header height
|
|
29
|
+
elt.style.setProperty('--top', height + 'px');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
20
33
|
//Composables
|
|
21
34
|
const { useProxyImageUrl } = useImageProxy();
|
|
22
35
|
|
|
23
36
|
//Computed
|
|
24
37
|
const backgroundDisplay = computed(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
if (!props.imgUrl) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
return `background-image: url('${useProxyImageUrl(
|
|
42
|
+
props.imgUrl,
|
|
43
|
+
"250",
|
|
44
|
+
)}');`;
|
|
32
45
|
});
|
|
33
46
|
</script>
|
|
34
47
|
|
|
35
48
|
<style lang="scss">
|
|
36
49
|
.octopus-app {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
.page-element.page-element-podcastmaker {
|
|
51
|
+
margin-top: 11rem;
|
|
52
|
+
}
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
position: absolute;
|
|
44
|
-
right: 0;
|
|
45
|
-
left: 0;
|
|
46
|
-
top: 0;
|
|
54
|
+
.page-element-title-container {
|
|
55
|
+
--top: 0;
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
background: black;
|
|
58
|
+
position: absolute;
|
|
59
|
+
right: 0;
|
|
60
|
+
left: 0;
|
|
61
|
+
top: var(--top);
|
|
62
|
+
|
|
63
|
+
.page-element-bg {
|
|
64
|
+
height: 15rem;
|
|
65
|
+
opacity: 0.5;
|
|
66
|
+
filter: blur(8px);
|
|
67
|
+
background-position: center;
|
|
68
|
+
background-repeat: no-repeat;
|
|
69
|
+
background-size: cover;
|
|
70
|
+
}
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
.page-element-title {
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset: 0;
|
|
75
|
+
z-index: 2;
|
|
76
|
+
display: flex;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
align-items: center;
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
h1 {
|
|
81
|
+
color: white !important;
|
|
82
|
+
font-size: 2rem;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
69
85
|
}
|
|
70
|
-
}
|
|
71
86
|
}
|
|
72
87
|
</style>
|