/o//commerce-media/accounts/-1/images/18463706?download=true
The following has evaluated to null or missing:
==> channel.items [in template "3192443#3192485#null" at line 5, column 64]
----
Tip: It's the step after the last dot that caused this error, not those before it.
----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----
----
FTL stack trace ("~" means nesting-related):
- Failed at: productImagesResponse = restClient.ge... [in template "3192443#3192485#null" at line 4, column 5]
----
1<#assign
2 channel = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?accountId=-1&filter=siteGroupId eq '${themeDisplay.getScopeGroupId()}'")
3
4 productImagesResponse = restClient.get(
5 "/headless-commerce-delivery-catalog/v1.0/channels/" + channel.items[0].id +
6 "/products/" + CPDefinition_cProductId.getData() + "/images?accountId=-1"
7 )
8
9 allProductImages = productImagesResponse.items![]
10 filteredProductImages = []
11>
12
13<#list allProductImages as image>
14 <#if image.galleryEnabled?? && image.galleryEnabled>
15 <#assign filteredProductImages += [image] />
16 </#if>
17</#list>
18
19<#assign totalCount = filteredProductImages?size >
20
21<#if filteredProductImages?has_content>
22<div class = "carousel-container">
23 <div class = "main-image-wrapper">
24 <button class = "nav-button prev" aria-label = "Previous Image">
25 <span class = "lexicon-icon-overwide"> <@clay["icon"] symbol = "angle-left" /></span>
26 </button>
27
28 <img alt = "${filteredProductImages[0].title?html}" id = "main-image" src = "${(filteredProductImages[0].src?replace("https://", "http://"))}" />
29
30 <button class="nav-button next" aria-label="Next Image">
31 <span class="lexicon-icon-overwide"> <@clay["icon"] symbol="angle-right" /></span>
32 </button>
33 </div>
34
35 <div class="thumbnails-wrapper">
36 <div class="thumbnails align-items-center"></div>
37
38 <#assign count = (totalCount?default(0)?number) />
39
40 <#if count gt 5>
41 <button class="view-full-gallery">
42 <span class="title">
43 ${languageUtil.get(locale, "full-gallery", "Full Gallery")}
44 </span>
45 <span class="subtitle">
46 ${count} ${languageUtil.get(locale, "photos", "Photos")}
47 </span>
48 </button>
49 </#if>
50 </div>
51 </div>
52</#if>
53<template id="modal-gallery">
54 <div class="modal-gallery-content">
55 <button class="modal-prev" data-role="modal-prev">
56 <@clay["icon"] symbol="angle-left" />
57 </button>
58
59 <img class="modal-image" data-role="modal-image" />
60
61 <button class="modal-next" data-role="modal-next">
62 <@clay["icon"] symbol="angle-right" />
63 </button>
64 </div>
65</template>
66
67<script ${nonceAttribute}>
68(function () {
69 let currentIndex = 0;
70 let images = [];
71
72 const carouselNextBtn = document.querySelector('.nav-button.next');
73 const carouselPrevBtn = document.querySelector('.nav-button.prev');
74 const carouselMainImage = document.getElementById('main-image');
75 const thumbnailsContainer = document.querySelector('.thumbnails');
76 const viewFullGalleryBtn = document.querySelector('.view-full-gallery');
77
78 function loadImages() {
79 images = [
80 <#list filteredProductImages as image>
81 {
82 src: "${(image.src?replace('https://', 'http://'))?js_string}",
83 alt: "${image.title?html?js_string}"
84 }<#if image_has_next>,</#if>
85 </#list>
86 ]
87 }
88
89 function renderThumbnails() {
90 const maxVisible = 5;
91 let start = currentIndex - 2;
92
93 if (start < 0) start = 0;
94 if (start > images.length - maxVisible) start = Math.max(images.length - maxVisible, 0);
95
96 const end = Math.min(images.length, start + maxVisible);
97
98 thumbnailsContainer.innerHTML = '';
99
100 for (let i = start; i < end; i++) {
101 const img = document.createElement('img');
102 img.className = 'thumbnail' + (i === currentIndex ? ' selected' : '');
103 img.src = images[i].src;
104 img.alt = images[i].alt;
105 img.dataset.index = i;
106 img.addEventListener('click', () => updateMainImage(i));
107 thumbnailsContainer.appendChild(img);
108 }
109 }
110
111 function updateMainImage(index) {
112 currentIndex = index;
113 carouselMainImage.src = images[index].src;
114 carouselMainImage.alt = images[index].alt;
115
116 carouselPrevBtn.disabled = index === 0;
117 carouselNextBtn.disabled = index === images.length - 1;
118
119 renderThumbnails();
120 }
121
122 function setupNavigationButtons() {
123 carouselPrevBtn.addEventListener('click', () => {
124 if (currentIndex > 0) updateMainImage(currentIndex - 1);
125 });
126
127 carouselNextBtn.addEventListener('click', () => {
128 if (currentIndex < images.length - 1) updateMainImage(currentIndex + 1);
129 });
130 }
131
132 function setupModalTriggers() {
133 carouselMainImage.addEventListener('click', () => openModalGallery(currentIndex));
134 if (viewFullGalleryBtn) {
135 viewFullGalleryBtn.addEventListener('click', () => openModalGallery(currentIndex));
136 }
137 }
138
139 function openModalGallery(startIndex) {
140 let current = startIndex;
141
142 const template = document.getElementById('modal-gallery');
143 const clone = template.content.cloneNode(true);
144 const container = document.createElement('div');
145 container.appendChild(clone);
146
147 Liferay.Util.openModal({
148 bodyHTML: container.innerHTML,
149 center: true,
150 headerHTML: '<h2 class="modal-gallery-header" id="modal-header-title"><@clay["icon"] symbol="picture"/> ${languageUtil.get(locale, "Image")} <span id="modal-index-display"></span></h2>',
151 size: "full-screen",
152 onOpen: () => {
153 const modalContainer = document.querySelector('.modal-content');
154 if (modalContainer) {
155 modalContainer.classList.add('custom-gallery-modal');
156 }
157
158 const modalImage = document.querySelector('[data-role="modal-image"]');
159 const modalNext = document.querySelector('[data-role="modal-next"]');
160 const modalPrev = document.querySelector('[data-role="modal-prev"]');
161 const indexDisplay = document.getElementById('modal-index-display');
162
163 function updateModalImage(index) {
164 const img = images[index];
165 modalImage.src = img.src;
166 modalImage.alt = img.alt;
167
168 modalNext.disabled = index === images.length - 1;
169 modalPrev.disabled = index === 0;
170
171 if (indexDisplay) {
172 indexDisplay.textContent = (index + 1) + ' ${languageUtil.get(locale, "of")} ' + images.length;
173 }
174 }
175
176 modalNext.addEventListener('click', () => {
177 if (current < images.length - 1) {
178 current++;
179 updateModalImage(current);
180 }
181 });
182
183 modalPrev.addEventListener('click', () => {
184 if (current > 0) {
185 current--;
186 updateModalImage(current);
187 }
188 });
189
190 updateModalImage(current);
191 }
192 });
193 }
194
195 function main() {
196 loadImages();
197 setupNavigationButtons();
198 setupModalTriggers();
199 updateMainImage(0);
200 }
201
202 main();
203})();
204</script>
205
206<style ${nonceAttribute}>
207.carousel-container img {
208 cursor: pointer;
209 object-fit: contain;
210}
211
212.custom-gallery-modal button:disabled {
213 cursor: default;
214 opacity: 0.4;
215 pointer-events: none;
216}
217
218.custom-gallery-modal {
219 background-color: #282934 !important;
220 border-bottom: none;
221 color: white !important;
222}
223
224.custom-gallery-modal .liferay-modal-body {
225 align-items: center;
226 display: flex;
227 justify-content: center;
228 position: relative;
229}
230
231.custom-gallery-modal .close {
232 color: white !important;
233 margin-right: 16px !important;
234}
235
236.lexicon-icon-overwide .lexicon-icon {
237 height: 2em;
238 margin: 0px !important;
239}
240
241.custom-gallery-modal{
242 height: 80% !important;
243}
244
245.main-image-wrapper {
246 align-items: center;
247 display:flex;
248 justify-content: center;
249 position: relative;
250 width: 902px;
251 height: 454px;
252}
253
254.main-image-wrapper img {
255 border-radius: 8px;
256 max-height: 100%;
257}
258
259.main-image-wrapper:hover .nav-button {
260 opacity: 1;
261 pointer-events: auto;
262}
263
264.main-image-wrapper:hover .nav-button:disabled{
265 cursor: default;
266 opacity: 0.4;
267}
268
269.modal-image {
270 aspect-ratio: 16/9;
271 object-fit: contain;;
272 border-radius: 8px;
273 max-width: 100vh !important;
274}
275
276.modal-gallery-header .lexicon-icon {
277 fill: #FFC124 !important;
278 margin-right: 8px !important;
279 width: 16px !important;
280}
281
282.modal-gallery-header {
283
284 padding: 16px !important;
285}
286
287.modal-prev,
288.modal-next {
289 align-items: center;
290 background: rgba(105, 102, 102, 0.4) !important;
291 border-radius: 50%;
292 border: none;
293 display: flex;
294 font-size: 1.6rem;
295 justify-content: center;
296 padding: 14px !important;
297 position: absolute;
298 top: 45%;
299}
300
301.modal-prev .lexicon-icon,
302.modal-next .lexicon-icon {
303 margin-top: 0px !important;
304}
305
306.modal-next {
307 right: 24px;
308}
309
310.modal-prev {
311 left: 24px;
312}
313
314.nav-button {
315 background: rgba(0,0,0,0.4);
316 border-radius: 50%;
317 border: none;
318 color: white;
319 cursor: pointer;
320 font-size: 1rem;
321 opacity: 0;
322 padding: 0 8px;
323 position: absolute;
324 top: 50%;
325 transform: translateY(-50%);
326 transition: opacity 0.3s ease;
327 user-select: none;
328}
329
330.nav-button.prev {
331 left: 10px;
332}
333
334.nav-button.next {
335 right: 10px;
336}
337
338.thumbnail {
339 border-radius: 12px;
340 border: 2px solid transparent;
341 cursor: pointer;
342 height: 86px;
343 object-fit: cover;
344 opacity: 0.6;
345 transition:
346 border-color 250ms ease-out,
347 opacity 250ms ease-out;
348 width: 142px;
349}
350
351.thumbnail.selected,
352.thumbnail:hover {
353 border-color: #8FB5FF;
354 opacity: 1;
355}
356
357.thumbnail.selected{
358 height: 102px;
359}
360
361.thumbnails {
362 display: flex;
363 gap: 8px;
364 overflow-x: auto;
365}
366
367.thumbnails-wrapper {
368 align-items: center;
369 display: flex;
370 justify-content: flex-start;
371 margin-top: 24px;
372 max-height: 86px;
373 max-width: 902px;
374}
375
376.view-full-gallery {
377 background-color: white;
378 border-radius: 12px;
379 border: 1px solid #E2E2E4;
380 color: #2563eb;
381 cursor: pointer;
382 display: flex;
383 flex-direction: column;
384 height: 86px;
385 justify-content: center;
386 margin-left: 8px;
387 min-width: 152px;
388 transition: background-color 0.3s ease,
389 box-shadow 0.3s ease,
390 border-color 250ms ease-out;
391}
392
393.view-full-gallery .subtitle {
394 color: #6b7280;
395 font-size: 12px;
396 font-weight: 400;
397 line-height: 1;
398}
399
400.view-full-gallery .title {
401 font-size: 16px;
402 font-weight: 600;
403 line-height: 1;
404 margin-bottom: 4px;
405}
406
407.view-full-gallery:hover {
408 background-color: #f3f4f6;
409 box-shadow: 0 2px 4px rgb(0 0 0 / 0.1);
410 border: 2px solid rgb(143, 181, 255);
411}
412</style>
Generally we have a requirement that on a particular day the users must be redirected to a page after login.
For that one solution is to create a hook and catch the post login event and redirect user to a specific page. But in this case someone has to deploy the hook at 00:00 hrs and undeploy the hook. This is a manual task and would require a lot of effort by the user.
To remove that we have developed a portlet where the user configure an event and set the landing page for a specific amount of time. Also after the event has been over the previous landing page will automatically come into action.
Generally we have a requirement that on a particular day the users must be redirected to a page after login.
For that one solution is to create a hook and catch the post login event and redirect user to a specific page. But in this case someone has to deploy the hook at 00:00 hrs and undeploy the hook. This is a manual task and would require a lot of effort by the user.
To remove that we have developed a portlet where the user configure an event and set the landing page for a specific amount of time. Also after the event has been over the previous landing page will automatically come into action.
Entwickler
Publisher Date
January 22, 2024
Deployment Method
Liferay Self-Hosted
Liferay PaaS
App Type
Version
Unterstützte Versionen
Standard Price
Help and Support
Link teilen
HTML Example
A paragraph is a self-contained unit of a discourse in writing dealing with a particular point or idea. Paragraphs are usually an expected part of formal writing, used to organize longer prose.