1import ListItem from '../../Resources/ListItem.js'; 2import SeriesCarousel from '../../Resources/SeriesCarousel.js'; 3import LoadingSpinner from '../../VueComponents/LoadingSpinner.js'; 4import weekendMessagesEndpoints from '../../utils/weekendMessagesEndpoints.js'; 5import resourcesUtils from '../../Resources/resourcesUtils.js'; 6import GridLayout from '../../VueComponents/Layouts/GridLayout.js'; 7import GridItem from '../../VueComponents/Layouts/GridItem.js'; 8import FilterCard from '../../VueComponents/Cards/FilterCard.js'; 9import FilterPanelMobile from '../../VueComponents/Cards/FilterPanelMobile.js'; 10 11export default { 12 name: 'WeekendMessagesListView', 13 components: { 14 ListItem, 15 SeriesCarousel, 16 LoadingSpinner, 17 GridLayout, 18 GridItem, 19 FilterCard, 20 FilterPanelMobile, 21 }, 22 props: { 23 healthyChurchDomain: { 24 type: String, 25 required: true, 26 }, 27 campusId: { 28 type: Number, 29 required: false, 30 default: null, 31 } 32 }, 33 data() { 34 return { 35 isLoading: true, 36 itemList: [], 37 sortBy: 'newest', 38 // Single source of truth for the series filter, shared by the carousel and 39 // the dropdown. null = "all". Holds the Sardius seriesAssetId string (e.g. 40 // "series_Live Sent") â the only key that matches across the series list and sermons. 41 selectedSeriesId: null, 42 // Stable reference so changing another filter doesn't rebuild this dropdown. 43 sortOptions: [ 44 { id: 'newest', text: 'Newest First' }, 45 { id: 'oldest', text: 'Oldest First' }, 46 { id: 'title', text: 'Title (A-Z)' }, 47 { id: 'titleReversed', text: 'Title (Z-A)' }, 48 ], 49 }; 50 }, 51 computed: { 52 resolvedCampusId() { 53 return this.campusId ?? 12; // Lake Forest 54 }, 55 hasNoMessages() { 56 // The feed came back with nothing â either an empty result set or a request 57 // that failed and was caught, leaving the list empty. Without messages there 58 // are no series to filter by either, so the whole grid has nothing to show. 59 return this.itemList.length === 0; 60 }, 61 selectedSeriesName() { 62 if (this.selectedSeriesId === null) return null; 63 const match = this.itemList.find((item) => item.seriesAssetId === this.selectedSeriesId); 64 return match?.seriesName ?? null; 65 }, 66 headingText() { 67 // Show the series name when filtered to one; otherwise the default heading. 68 return this.selectedSeriesName ?? 'Latest Messages'; 69 }, 70 filterDropdowns() { 71 return [ 72 { 73 key: 'sort', 74 label: 'Sort By', 75 ariaLabel: 'Sort messages by', 76 value: this.sortBy, 77 default: 'newest', 78 options: this.sortOptions, 79 }, 80 { 81 key: 'series', 82 label: 'Series', 83 ariaLabel: 'Filter messages by series', 84 value: this.selectedSeriesId ?? 'all', 85 default: 'all', 86 options: this.seriesOptions, 87 }, 88 ]; 89 }, 90 seriesOptions() { 91 // Dedupe by seriesAssetId so the dropdown speaks the same language as the 92 // carousel and the sermon feed. Keyed by assetId; value is the display name. 93 const seriesByAssetId = new Map(); 94 this.itemList.forEach((item) => { 95 if (item.seriesAssetId && item.seriesName && !seriesByAssetId.has(item.seriesAssetId)) { 96 seriesByAssetId.set(item.seriesAssetId, item.seriesName); 97 } 98 }); 99 100 const options = [...seriesByAssetId.entries()] 101 .map(([id, text]) => ({ id, text })) 102 .sort((a, b) => a.text.localeCompare(b.text)); 103 104 return [{ id: 'all', text: 'All Series' }, ...options]; 105 }, 106 sortedItemList() { 107 let items = [...this.itemList]; 108 109 if (this.selectedSeriesId !== null) { 110 items = items.filter((item) => item.seriesAssetId === this.selectedSeriesId); 111 } 112 113 switch (this.sortBy) { 114 case 'oldest': 115 return items.sort((a, b) =>
115 this.getSortDate(a) - this.getSortDate(b)); 116 case 'title': 117 return items.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); 118 case 'titleReversed': 119 return items.sort((a, b) => (b.name ?? '').localeCompare(a.name ?? '')); 120 case 'newest': 121 default: 122 return items.sort((a, b) => this.getSortDate(b) - this.getSortDate(a)); 123 } 124 }, 125 }, 126 mounted() { 127 this.fetchLatestSermons(); 128 }, 129 methods: { 130 fetchLatestSermons(pageNumber = 0, pageSize = 250, endDate = null) { 131 this.isLoading = true; 132 const url = weekendMessagesEndpoints.getLatestSermons(this.healthyChurchDomain, pageNumber, pageSize, this.resolvedCampusId, endDate); 133 window.sbDebug && console.log('[WeekendMessages] getLatestSermons:', url); 134 axios 135 .get(url) 136 .then((response) => { 137 // Guard: the feed returns campusServiceTemplate: null when a campus/domain 138 // has no sermons (e.g. the dev HC domain), which would otherwise crash 139 // seriesOptions/sortedItemList (forEach / spread on null). 140 this.itemList = response.data.campusServiceTemplate ?? []; 141 if (this.hasNoMessages) { 142 // The request succeeded but carried nothing â the error state the 143 // visitor sees is the feed being empty, not a failed call. 144 console.error( 145 `[WeekendMessagesListView] Showing the error state: the sermon feed returned no messages for campus ${this.resolvedCampusId}. Requested ${url}`, 146 response.data 147 ); 148 } 149 }) 150 .catch((error) => { 151 console.error( 152 `[WeekendMessagesListView] Showing the error state: the sermon feed request failed. Requested ${url}`, 153 error 154 ); 155 }) 156 .finally(() => { 157 this.isLoading = false; 158 }) 159 }, 160 getSermonDescription(sermonItem) { 161 return resourcesUtils.getSpeakerDateString(sermonItem); 162 }, 163 getSermonTags(sermonItem) { 164 return resourcesUtils.getTags(sermonItem, ["weekendmessages"]); 165 }, 166 getSermonLink(sermonItem) { 167 const origin = window.location.origin; 168 return resourcesUtils.generateMediaDetailsUrl(sermonItem, origin); 169 }, 170 getSortDate(sermonItem) { 171 return sermonItem?.occurrences?.[0]?.actualDate ?? 0; 172 }, 173 onSeriesSelected(series) { 174 // series is the selected series object, or null when the user deselects. 175 // Filter by seriesAssetId â the Sardius key that matches sermon.seriesAssetId. 176 this.selectedSeriesId = series?.seriesAssetId ?? null; 177 }, 178 onDropdownChanged(key, value) { 179 switch (key) { 180 case 'sort': 181 this.sortBy = value; 182 break; 183 case 'series': 184 // Values are the seriesAssetId string (or "all"). 185 this.selectedSeriesId = value === 'all' ? null : value; 186 break; 187 } 188 }, 189 }, 190 template: ` 191 <section class="courses-list-view"> 192 <div class="clamp"> 193 <LoadingSpinner v-show="isLoading" /> 194 <Transition> 195 <GridLayout 196 v-show="!isLoading" 197 class="clv-grid-layout" 198 :columns="2" 199 gridTemplateColumns="1fr 338px" 200 > 201 <!-- Nothing came back from the feed. span="all" rather than a 202 number so it spans both columns whatever the grid resolves to. --> 203 <GridItem 204 v-if="hasNoMessages" 205 span="all" 206 class="list-empty-state" 207 role="status" 208 > 209 <h3>Oops!</h3> 210 <p class="message-text">An error has occurred, please refresh the page and try again.</p> 211 </GridItem> 212 <!-- With no messages there are no series to filter by either, so the 213 carousel, heading, filters and list all have nothing to show. --> 214 <template v-else> 215 <GridItem :span="2">
216 <SeriesCarousel 217 :healthyChurchDomain="healthyChurchDomain" 218 :selectedSeriesId="selectedSeriesId" 219 @series-selected="onSeriesSelected" 220 /> 221 </GridItem> 222 <GridItem :span="2"> 223 <h3 style="margin: 0">{{ headingText }}</h3> 224 </GridItem> 225 <FilterPanelMobile 226 :dropdowns="filterDropdowns" 227 idPrefix="weekendMessagesMobile" 228 @dropdown-changed="onDropdownChanged" 229 /> 230 <GridItem 231 class="list-item-container" 232 :span="1" 233 > 234 <ListItem 235 v-for="item in sortedItemList" 236 :key="item.id" 237 :imageUrl="item.thumbnailImage" 238 :wideImageUrl="item.appHeaderImage" 239 :subHeader="item.seriesName" 240 :title="item.name" 241 :description="getSermonDescription(item)" 242 :tags="getSermonTags(item)" 243 ctaLabel="Watch Message" 244 :ctaUrl="getSermonLink(item)" 245 :ctaIsExternal="false" 246 /> 247 </GridItem> 248 <FilterCard 249 :dropdowns="filterDropdowns" 250 idPrefix="weekendMessages" 251 hideOnSingleColumn 252 @dropdown-changed="onDropdownChanged" 253 /> 254 </template> 255 </GridLayout> 256 </Transition> 257 </div> 258 </section> 259 `, 260};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.