|
1 | | -<template> |
2 | | - <div> |
3 | | - <div class="breadcrumb flex" v-if="showingAddon"> |
4 | | - <button |
5 | | - @click="showingAddon = false" |
6 | | - class="-m-2 flex flex-initial items-center p-2 text-xs text-gray-700 hover:text-gray-900 dark:text-dark-175 dark:hover:text-dark-100" |
7 | | - > |
8 | | - <svg-icon name="micro/chevron-right" class="h-6 w-4 rotate-180" /> |
9 | | - <span v-text="__('Addons')" /> |
10 | | - </button> |
11 | | - </div> |
12 | | - |
13 | | - <div class="mb-6 flex" v-if="!showingAddon"> |
14 | | - <h1 class="flex-1" v-text="__('Addons')" /> |
15 | | - </div> |
16 | | - |
17 | | - <div v-if="error" class="card text-sm"> |
18 | | - {{ __('messages.addon_list_loading_error') }} |
19 | | - </div> |
| 1 | +<script setup> |
| 2 | +import { ref, computed } from 'vue'; |
| 3 | +import { |
| 4 | + Header, |
| 5 | + Badge, |
| 6 | + Card, |
| 7 | + Panel, |
| 8 | + PanelFooter, |
| 9 | + Tabs, |
| 10 | + TabList, |
| 11 | + TabTrigger, |
| 12 | + Listing, |
| 13 | + ListingSearch as Search, |
| 14 | + ListingPagination as Pagination, |
| 15 | +} from '@statamic/ui'; |
| 16 | +
|
| 17 | +const props = defineProps(['domain', 'endpoints', 'installCount']); |
| 18 | +
|
| 19 | +const requestUrl = cp_url('/api/addons'); |
| 20 | +const filter = ref('all'); |
| 21 | +const showingAddon = ref(false); |
| 22 | +const unlisted = ref([]); |
| 23 | +
|
| 24 | +const additionalParameters = computed(() => ({ |
| 25 | + installed: filter.value === 'installed' ? 1 : 0, |
| 26 | +})); |
| 27 | +
|
| 28 | +function getCover(addon) { |
| 29 | + return addon.assets.length |
| 30 | + ? addon.assets[0].url |
| 31 | + : 'https://statamic.com/images/img/marketplace/placeholder-addon.png'; |
| 32 | +} |
20 | 33 |
|
21 | | - <div v-if="initializing" class="card p-6 text-center"> |
22 | | - <loading-graphic /> |
23 | | - </div> |
| 34 | +function getPriceRange(addon) { |
| 35 | + let [low, high] = addon.price_range; |
| 36 | + low = low ? `$${low}` : 'Free'; |
| 37 | + high = high ? `$${high}` : 'Free'; |
| 38 | + return low == high ? low : `${low} - ${high}`; |
| 39 | +} |
24 | 40 |
|
25 | | - <data-list :rows="rows" v-if="!initializing && !showingAddon" v-slot="{ rows: addons }"> |
26 | | - <div class=""> |
27 | | - <div class="card p-0"> |
28 | | - <div class="border-b px-4 text-sm dark:border-dark-900"> |
29 | | - <button |
30 | | - class="data-list-filter-link" |
31 | | - :class="{ active: filter === 'all' }" |
32 | | - @click="filter = 'all'" |
33 | | - v-text="__('All')" |
34 | | - /> |
35 | | - <button |
36 | | - class="data-list-filter-link" |
37 | | - :class="{ active: filter === 'installed' }" |
38 | | - @click="filter = 'installed'" |
39 | | - > |
40 | | - {{ __('Installed') }} |
41 | | - <span class="badge" v-if="installCount">{{ installCount }}</span> |
42 | | - </button> |
43 | | - </div> |
| 41 | +function showAddon(addon) { |
| 42 | + showingAddon.value = addon; |
| 43 | + window.scrollTo(0, 0); |
| 44 | +} |
| 45 | +</script> |
44 | 46 |
|
45 | | - <div class="p-2"> |
46 | | - <data-list-search ref="search" v-model="searchQuery" /> |
47 | | - </div> |
48 | | - </div> |
| 47 | +<template> |
| 48 | + <div> |
| 49 | + <Header v-if="!showingAddon" :title="__('Addons')" icon="addons" /> |
| 50 | + <Listing |
| 51 | + v-if="!showingAddon" |
| 52 | + :url="requestUrl" |
| 53 | + :additional-parameters="additionalParameters" |
| 54 | + v-slot="{ items: addons, loading }" |
| 55 | + > |
| 56 | + <Tabs v-model="filter"> |
| 57 | + <TabList> |
| 58 | + <TabTrigger :text="__('All')" name="all" /> |
| 59 | + <TabTrigger name="installed"> |
| 60 | + {{ __('Installed') }} |
| 61 | + <Badge size="sm" v-if="installCount" :text="installCount" /> |
| 62 | + </TabTrigger> |
| 63 | + </TabList> |
| 64 | + </Tabs> |
| 65 | + |
| 66 | + <div class="py-3"> |
| 67 | + <Search /> |
| 68 | + </div> |
49 | 69 |
|
50 | | - <div class="addon-grid my-8" :class="{ 'opacity-50': loading }"> |
51 | | - <div |
52 | | - class="addon-card relative h-full cursor-pointer rounded-sm bg-white text-gray-800 shadow dark:bg-dark-600 dark:text-dark-150 dark:shadow-lg" |
| 70 | + <Panel> |
| 71 | + <div class="grid grid-cols-3 gap-2" :class="{ 'opacity-50': loading }"> |
| 72 | + <Card |
| 73 | + class="relative cursor-pointer" |
53 | 74 | v-for="addon in addons" |
54 | 75 | :key="addon.id" |
55 | 76 | @click="showAddon(addon)" |
56 | 77 | > |
57 | | - <span |
58 | | - class="badge absolute top-0 mt-2 ltr:left-0 ltr:ml-2 rtl:right-0 rtl:mr-2" |
| 78 | + <Badge |
59 | 79 | v-if="addon.installed" |
60 | | - >Installed</span |
61 | | - > |
| 80 | + :text="__('Installed')" |
| 81 | + class="absolute top-0 mt-2 ltr:left-0 ltr:ml-2 rtl:right-0 rtl:mr-2" |
| 82 | + /> |
62 | 83 | <div |
63 | 84 | class="h-48 rounded-t bg-cover bg-center" |
64 | 85 | :style="'background-image: url(\'' + getCover(addon) + '\')'" |
|
68 | 89 | <img |
69 | 90 | :src="addon.seller.avatar" |
70 | 91 | :alt="addon.seller.name" |
71 | | - class="relative -mt-8 inline h-14 w-14 rounded-full border-2 border-white bg-white dark:border-dark-600 dark:bg-dark-600" |
| 92 | + class="dark:border-dark-600 dark:bg-dark-600 relative -mt-8 inline h-14 w-14 rounded-full border-2 border-white bg-white" |
72 | 93 | /> |
73 | 94 | </a> |
74 | 95 | <div class="addon-card-title mb-2 text-center text-lg font-bold">{{ addon.name }}</div> |
75 | | - <p class="mb-4 text-gray dark:text-dark-175" v-text="getPriceRange(addon)" /> |
| 96 | + <p class="text-gray dark:text-dark-175 mb-4" v-text="getPriceRange(addon)" /> |
76 | 97 | <p v-text="addon.summary" class="text-sm"></p> |
77 | 98 | </div> |
78 | | - </div> |
| 99 | + </Card> |
79 | 100 | </div> |
80 | | - |
81 | | - <data-list-pagination :resource-meta="meta" @page-selected="setPage"></data-list-pagination> |
82 | | - </div> |
83 | | - </data-list> |
84 | | - |
| 101 | + <PanelFooter> |
| 102 | + <Pagination /> |
| 103 | + </PanelFooter> |
| 104 | + </Panel> |
| 105 | + </Listing> |
85 | 106 | <template v-if="unlisted.length && !showingAddon"> |
86 | 107 | <h6 class="mt-8">{{ __('Unlisted Addons') }}</h6> |
87 | 108 | <div class="card mt-2 p-0"> |
|
95 | 116 | </table> |
96 | 117 | </div> |
97 | 118 | </template> |
98 | | - |
99 | | - <addon-details v-if="showingAddon" :addon="showingAddon" :cover="getCover(showingAddon)" /> |
| 119 | + <addon-details |
| 120 | + v-if="showingAddon" |
| 121 | + :addon="showingAddon" |
| 122 | + :cover="getCover(showingAddon)" |
| 123 | + @close="showingAddon = false" |
| 124 | + /> |
100 | 125 | </div> |
101 | 126 | </template> |
102 | | - |
103 | | -<style> |
104 | | -.addon-grid { |
105 | | - display: grid; |
106 | | - grid-gap: 32px; |
107 | | - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); |
108 | | - grid-auto-flow: dense; |
109 | | -} |
110 | | -</style> |
111 | | - |
112 | | -<script> |
113 | | -export default { |
114 | | - props: ['domain', 'endpoints', 'installCount'], |
115 | | -
|
116 | | - data() { |
117 | | - return { |
118 | | - initializing: true, |
119 | | - loading: true, |
120 | | - rows: [], |
121 | | - meta: {}, |
122 | | - searchQuery: '', |
123 | | - filter: 'all', |
124 | | - page: 1, |
125 | | - showingAddon: false, |
126 | | - error: false, |
127 | | - unlisted: [], |
128 | | - }; |
129 | | - }, |
130 | | -
|
131 | | - computed: { |
132 | | - params() { |
133 | | - return { |
134 | | - page: this.page, |
135 | | - q: this.searchQuery, |
136 | | - installed: this.filter === 'installed' ? 1 : 0, |
137 | | - }; |
138 | | - }, |
139 | | -
|
140 | | - loaded() { |
141 | | - return !this.loading && !this.error; |
142 | | - }, |
143 | | - }, |
144 | | -
|
145 | | - watch: { |
146 | | - page() { |
147 | | - this.getAddons(); |
148 | | - }, |
149 | | -
|
150 | | - searchQuery() { |
151 | | - this.page = 1; |
152 | | - this.getAddons(); |
153 | | - }, |
154 | | -
|
155 | | - filter() { |
156 | | - this.page = 1; |
157 | | - this.getAddons(); |
158 | | - }, |
159 | | -
|
160 | | - loading: { |
161 | | - immediate: true, |
162 | | - handler(loading) { |
163 | | - this.$progress.loading('addon-list', loading); |
164 | | - }, |
165 | | - }, |
166 | | - }, |
167 | | -
|
168 | | - created() { |
169 | | - this.rows = this.getAddons(); |
170 | | -
|
171 | | - this.$events.$on('composer-finished', this.getAddons); |
172 | | - }, |
173 | | -
|
174 | | - methods: { |
175 | | - getAddons() { |
176 | | - this.loading = true; |
177 | | -
|
178 | | - this.$axios |
179 | | - .get(cp_url('/api/addons'), { params: this.params }) |
180 | | - .then((response) => { |
181 | | - this.loading = false; |
182 | | - this.initializing = false; |
183 | | - this.rows = response.data.data; |
184 | | - this.meta = response.data.meta; |
185 | | - this.unlisted = response.data.unlisted ?? []; |
186 | | -
|
187 | | - if (this.showingAddon) { |
188 | | - this.refreshShowingAddon(); |
189 | | - } |
190 | | - }) |
191 | | - .catch((e) => { |
192 | | - this.loading = false; |
193 | | - this.error = true; |
194 | | - this.$toast.error(__('Something went wrong')); |
195 | | - }); |
196 | | - }, |
197 | | -
|
198 | | - setPage(page) { |
199 | | - this.page = page; |
200 | | - }, |
201 | | -
|
202 | | - refreshShowingAddon() { |
203 | | - this.showingAddon = this.rows.find((row) => row.id === this.showingAddon.id); |
204 | | -
|
205 | | - this.$events.$emit('addon-refreshed'); |
206 | | - }, |
207 | | -
|
208 | | - getCover(addon) { |
209 | | - return addon.assets.length |
210 | | - ? addon.assets[0].url |
211 | | - : 'https://statamic.com/images/img/marketplace/placeholder-addon.png'; |
212 | | - }, |
213 | | -
|
214 | | - getPriceRange(addon) { |
215 | | - let [low, high] = addon.price_range; |
216 | | - low = low ? `$${low}` : 'Free'; |
217 | | - high = high ? `$${high}` : 'Free'; |
218 | | - return low == high ? low : `${low} - ${high}`; |
219 | | - }, |
220 | | -
|
221 | | - showAddon(addon) { |
222 | | - this.showingAddon = addon; |
223 | | - window.scrollTo(0, 0); |
224 | | - }, |
225 | | - }, |
226 | | -}; |
227 | | -</script> |
0 commit comments