aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
blob: 4e20a55bcde3a20a18602016aeb9e89fb810c402 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<template>
	<div class="app-discover">
		<NcEmptyContent v-if="hasError"
			:name="t('settings', 'Nothing to show')"
			:description="t('settings', 'Could not load section content from app store.')">
			<template #icon>
				<NcIconSvgWrapper :path="mdiEyeOff" :size="64" />
			</template>
		</NcEmptyContent>
		<NcEmptyContent v-else-if="elements.length === 0"
			:name="t('settings', 'Loading')"
			:description="t('settings', 'Fetching the latest news…')">
			<template #icon>
				<NcLoadingIcon :size="64" />
			</template>
		</NcEmptyContent>
		<template v-else>
			<component :is="getComponent(entry.type)"
				v-for="entry, index in elements"
				:key="entry.id ?? index"
				v-bind="entry" />
		</template>
	</div>
</template>

<script setup lang="ts">
import type { IAppDiscoverElements } from '../../constants/AppDiscoverTypes.ts'

import { mdiEyeOff } from '@mdi/js'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { defineAsyncComponent, defineComponent, onBeforeMount, ref } from 'vue'

import axios from '@nextcloud/axios'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'

import logger from '../../logger'
import { parseApiResponse, filterElements } from '../../utils/appDiscoverParser.ts'

const PostType = defineAsyncComponent(() => import('./PostType.vue'))
const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))
const ShowcaseType = defineAsyncComponent(() => import('./ShowcaseType.vue'))

const hasError = ref(false)
const elements = ref<IAppDiscoverElements[]>([])

/**
 * Shuffle using the Fisher-Yates algorithm
 * @param array The array to shuffle (in place)
 */
const shuffleArray = <T, >(array: T[]): T[] => {
	for (let i = array.length - 1; i > 0; i--) {
		const j = Math.floor(Math.random() * (i + 1));
		[array[i], array[j]] = [array[j], array[i]]
	}
	return array
}

/**
 * Load the app discover section information
 */
onBeforeMount(async () => {
	try {
		const { data } = await axios.get<Record<string, unknown>[]>(generateUrl('/settings/api/apps/discover'))
		if (data.length === 0) {
			logger.info('No app discover elements available (empty response)')
			hasError.value = true
			return
		}
		// Parse data to ensure dates are useable and then filter out expired or future elements
		const parsedElements = data.map(parseApiResponse).filter(filterElements)
		// Shuffle elements to make it looks more interesting
		const shuffledElements = shuffleArray(parsedElements)
		// Sort pinned elements first
		shuffledElements.sort((a, b) => (a.order ?? Infinity) < (b.order ?? Infinity) ? -1 : 1)
		// Set the elements to the UI
		elements.value = shuffledElements
	} catch (error) {
		hasError.value = true
		logger.error(error as Error)
		showError(t('settings', 'Could not load app discover section'))
	}
})

const getComponent = (type) => {
	if (type === 'post') {
		return PostType
	} else if (type === 'carousel') {
		return CarouselType
	} else if (type === 'showcase') {
		return ShowcaseType
	}
	return defineComponent({
		mounted: () => logger.error('Unknown component requested ', type),
		render: (h) => h('div', t('settings', 'Could not render element')),
	})
}
</script>

<style scoped lang="scss">
.app-discover {
	max-width: 1008px; /* 900px + 2x 54px padding for the carousel controls */
	margin-inline: auto;
	padding-inline: 54px;
	/* Padding required to make last element not bound to the bottom */
	padding-block-end: var(--default-clickable-area, 44px);

	display: flex;
	flex-direction: column;
	gap: var(--default-clickable-area, 44px);
}
</style>