/o//commerce-media/accounts/-1/images/18463706?download=true

EventLandingPortlet
Data Modeling, Process & Business Logic
18463566
Fehler bei der Verarbeitung der Vorlage.
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> 
Beschreibung
Event Landing Portlet
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.
Event Landing Portlet
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.
DEVELOPER

Entwickler


Publisher Date

January 22, 2024


Deployment Method

Liferay Self-Hosted

Liferay PaaS


App Type

DXP

Version

1

Unterstützte Versionen

6.2

Standard Price

Free

Help and Support


Link teilen

DEVELOPER
21.11.24 17:46
Published date
21.11.24 17:46
Published Date
21.11.24 17:46
SUPPORTED OFFERINGS
Liferay PaaS
Supported Versions
6.2
Resource Requirements
Edition
Community
PRICE
Free
help & support
SHARE LINK
Copy & Share

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.