1/**-------------------------------------------------------------------------------------- 2 * * 3 * the page this script is used on MUST have the carousel-button.js script linked * 4 * * 5 * -------------------------------------------------------------------------------------- 6 */ 7 8//variables controlling the news section 9var featuredNewsBool = false; 10var newsId = 0; 11 12//bool for alternating colours of bgs/labels 13var pink = true; 14 15 16/** 17 * Alternate Colours 18 * 19 * Outputs blue or pink depending on the previous colour. 20 * 21 * @return string array [bgColour, labelColour] 22 */ 23function alternateColours() { 24 if (pink) { 25 pink = !pink; 26 return ['#F4E8ED', "pink"]; 27 } else { 28 pink = !pink; 29 return ['#DDEDEF', "teal"]; 30 } 31} 32 33async function scrollSection(containerId, params = {}) { 34 pink = false; 35 36 if (Object.keys(params).length === 0 || typeof containerId !== "string") { 37 return; 38 } 39 40 // header 41 let headerParams = params.headerParams ?? {}; 42 43 let headerCode = ``; 44 if (Object.keys(headerParams).length > 0) { 45 headerCode = `<div class="scroll-section-header flex justify-between items-center lg:gap-5 ga
45p-2 flex-row lg:mb-6 px-4"> 46 ${createTitle(headerParams["title"], headerParams["colour"])} 47 ${createMoreButton(headerParams["buttonText"], headerParams["buttonLink"], headerParams["colour"])} 48 </div>` 49 } 50 51 // card style 52 let cardType = params.sectionType ?? ""; 53 54 // data source 55 let jsonData = params.jsonData ?? []; 56 let apiLinks = params.apiLinks ?? []; 57 let sourceData = (jsonData.length !== 0 ? jsonData : await getCardData(apiLinks, cardType, [])); 58 59 const exclude = params.exclude ?? []; 60 if (exclude.length > 0) { 61 sourceData = sourceData.filter(item => { 62 if (!item.types) return true; 63 return !item.types.some(t => exclude.includes(t)); 64 }); 65 } 66 67 if (sourceData.length === 0) { 68 if (apiLinks.length !== 0) { 69 let placeholder = ""; 70 71 switch (cardType) { 72 case "advert": 73 placeholder = "student offers"; 74 break; 75 case "blogs": 76 placeholder = "blog posts"; 77 break; 78 default: 79 placeholder = cardType; 80 } 81 82 $("#"+containerId).append(headerCode); 83 $("#"+containerId).append(`<p class="mt-0 pt-0 main-blurb">There are no ${placeholder} right now - check again later!</p>`); 84 } 85 86 return; 87 } 88 89 // Optional click handler for cards (e.g. opening a modal instead of navigating) 90 let onCardClick = params.onCardClick ?? null; 91 92 let cardCode = sourceData.map(item => cardTemplate(item, cardType, containerId)).join(''); 93 94 //arrow variables 95 let arrowContainerName = containerId + "FeedContainer"; 96 let scrollContainerName = containerId + "Grid"; 97 let startArrowDirection = "left"; 98 let endArrowDirection = "right"; 99 let scrollDistance = 344 * 2; 100 101 if (cardType === "full" || cardType === "slide") { 102 scrollDistance = "auto"; 103 } 104 105 if (cardType === "news") { 106 startArrowDirection = "up"; 107 endArrowDirection = "down"; 108 scrollDistance = 195; 109 } 110 111 let gridStyle = `mt-4 mb-4 lg:py-4 px-4 mx-auto`; 112 let wrap = params.wrap ?? false; 113 114 if (cardType === "news" && window.innerWidth >= 1024) { 115 gridStyle = `grid items-center overflow-y-auto h-[${175 * 3 + 20 * 2}px] lg:py-0`; 116 } else if (cardType === "advert" || wrap) { 117 gridStyle += ` flex flex-wrap gap-4 justify-center`; 118 } else if (cardType === "slide") { 119 gridStyle = "flex flex-nowrap overflow-x-auto lg:!py-2 lg:!px-8 lg:!mx-0 items-stretch"; 120 } else { 121 gridStyle += ` flex flex-nowrap overflow-x-auto`; 122 } 123 124 // Freshers card types: remove horizontal scroller class on mobile 125 let scrollerClass = window.innerWidth >= 1024 ? 'csu-content-scroller' : ''; 126 127 // Build section with placeholder container for arrows 128 let scrollSection = ` 129 <div id="${containerId}GridContainer" class="w-full flex items-center flex-row relative ${cardType === "news" && window.innerWidth >= 1024 ? "lg:flex-col" : ""}"> 130 131 ${!wrap ? `<div id="${arrowContainerName}Left" ${cardType === "news" ? 'class="w-full lg:flex justify-center hidden"' : ""}></div>` : ""} 132 133 <div id="${containerId}Grid" class="${scrollerClass} 134 snap-x ${gridStyle} ${cardType === "slide" || cardType === "full" ? "w-full": "w-fit"} 135 pb-2 ${cardType === "advert" || cardType === "slide" ? "lg:gap-8 gap-4" :"gap-5"}"> 136 ${cardCode} 137 </div> 138 139 ${!wrap ? `<div id="${arrowContainerName}Right" ${cardType === "news" ? 'class="w-full lg:flex justify-center hidden"' : ""}></div>` : ""} 140 </div> 141 ${!wrap ? `<div id="${arrowContainerName}Mobile" class="flex justify-end gap-2 mt-3 lg:hidden px-1"></div>` : ""}`; 142 143 // News section rules 144 if (cardType === "news" && window.innerWidth >= 1024) { 145 scrollSection = ` 146 <div class="lg:gap-7 grid row-start-2 lg:grid-cols-3 mt-4 lg:mt-6 mb-4 lg:py-4"> 147 <div id="newsFeedfeatured" class="hidden lg:block col-end-3 col-start-1"> 148 <a class="hover:no-underline" href="${sourceData[0]["link"]}"> 149 <div class="rounded-2xl font-union-semibold filter transition ease-in-out duration-300" 150 style="background-image: url('${sourceData[0]["fullImg"]}'); 151 background-repeat: no-repeat;background-size: cover; 152 height: 570px;width: auto;background-position-x: center;background-position-y: center;"> 153 <div class=" bg-union-pink lg:w-fit min-h-[100px] flex items-center py-7 mr-7 pl-7 pr-12 relative rounded-r-full top-[350px]">
154 <span class="font-union-semibold font-union text-xl lg:text-4xl text-white"> 155 ${titleDecider(sourceData[0]["title"])} 156 </span> 157 </div> 158 </div> 159 </a> 160 </div> 161 162 <div class="col-start-3 flex flex-row lg:flex-col items-center relative lg:h-[570px]"> 163 ${scrollSection} 164 </div> 165 </div>`; 166 } 167 168 let wrapper = ` 169 <div class="tilegrid m-auto max-w-screen-2xl lg:mb-5 lg:mt-5"> 170 ${headerCode} 171 ${scrollSection} 172 </div>`; 173 174 $("#" + containerId).append(wrapper); 175 176 featuredNewsBool = false; 177 178 requestAnimationFrame(() => { 179 requestAnimationFrame(() => { 180 createBothArrows2({ 181 wrap, 182 cardType, 183 arrowContainerName, 184 scrollContainerName, 185 scrollDistance, 186 startArrowDirection, 187 endArrowDirection, 188 containerId, 189 }); 190 191 // Bind click handler for cards that use onCardClick instead of <a> links 192 if (onCardClick) { 193 $(`#${containerId}Grid`).find('[data-card-id]').on('click', function () { 194 const cardId = decodeURIComponent($(this).attr('data-card-id')); 195 const item = sourceData.find(d => (d.id || '') === cardId); 196 if (item) onCardClick(item); 197 }); 198 } 199 }); 200 }); 201} 202 203 204// Delay arrow creation until container is visible 205function createBothArrows2({ 206 wrap, 207 cardType, 208 arrowContainerName, 209 scrollContainerName, 210 scrollDistance, 211 startArrowDirection, 212 endArrowDirection, 213 containerId, 214}) { 215 const gridSelector = "#" + containerId + "Grid" 216 217 // Helper: wait until element is visible and has layout. 218 // Fast path: rAF polling for the first ~5s covers elements that are already 219 // visible or become visible quickly. Slow path: if the budget exhausts (e.g. 220 // the element is inside a collapsed accordion the user hasn't opened yet), 221 // hand off to a ResizeObserver on the parent so the callback still fires 222 // whenever the accordion opens â no matter how long that takes. 223 function waitUntilVisible(selector, callback) { 224 const el = document.querySelector(selector); 225 if (!el) return; 226 227 let frames = 0; 228 function check() { 229 if (el.offsetWidth > 0 && el.offsetHeight > 0) { 230 callback(); 231 } else if (frames++ < 300) { 232 requestAnimationFrame(check); 233 } else if ('ResizeObserver' in window) { 234 const observer = new ResizeObserver(() => { 235 if (el.offsetWidth > 0 && el.offsetHeight > 0) { 236 observer.disconnect(); 237 callback(); 238 } 239 }); 240 observer.observe(el.parentElement || el); 241 } 242 } 243 244 check(); 245 } 246 247 // Wait for the grid to be visible and ready 248 waitUntilVisible(gridSelector, () => { 249 const el = document.querySelector(gridSelector); 250 const isWide = window.innerWidth >= 1024; 251 252 function createDesktopArrows() { 253 $("#" + arrowContainerName + "Left").append( 254 createButton( 255 startArrowDirection, 256 "arrow-" + startArrowDirection + "-" + containerId, 257 arrowContainerName, 258 scrollContainerName, 259 scrollDistance 260 ) 261 ); 262 $("#" + arrowContainerName + "Right").append( 263 createButton( 264 endArrowDirection, 265 "arrow-" + endArrowDirection + "-" + containerId, 266 arrowContainerName, 267 scrollContainerName, 268 scrollDistance 269 ) 270 ); 271 } 272 273 const needsArrows = !wrap && isWide && cardType !== "news"; 274 275 if (!wrap && cardType === "news" && isWide) { 276 createDesktopArrows(); 277 } else if (needsArrows) { 278 if (isOverflownHorizontal(el)) { 279 createDesktopArrows(); 280 } else if ('ResizeObserver' in window) { 281 // On slow devices, layout may not be finalised when the initial 282 // overflow check runs. Watch for the grid growing and retry. 283 const observer = new ResizeObserver(() => { 284 if (isOverflownHorizontal(el)) { 285 observer.disconnect(); 286 createDesktopArrows(); 287 } 288 }); 289 observer.observe(el); 290 setTimeout(() => observer.disconnect(), 5000); 291 } 292 } 293 294 // Mobile arrows â always created for non-wrap, non-news horizontal sections 295 if (!wrap) { 296 createMobileArrows(containerId, scrollContainerName); 297 } 298 });
299} 300 301 302/** 303 * Card Template 304 * 305 * Generates a card whose style depends on the card's type. 306 * 307 * @param dictionary item dictionary of the card data 308 * @param string type the type of the card template 309 * @param string containerId ID of the tiledgrid containing the cards 310 * 311 * @return string HTML string containing the generated card code 312 */ 313function cardTemplate(item, type, containerId) { 314 let bgColour = ""; 315 let labelColour = ""; 316 let hoverBgColour = ""; 317 let bgImage = (item["bgImage"] ? item["bgImage"].replace(/\(/g, '\\(').replace(/\)/g, '\\)') : ""); 318 let img = (item["img"] ? item["img"] : ""); 319 let alt = (item["alt"] ? item["alt"] : ""); 320 let desc = (item["desc"] ? processLanguage(item["desc"]) : ""); 321 let cardSize = "lg:max-w-[320px] lg:h-[400px] lg:min-w-[320px] max-w-[240px] min-w-[240px] h-[300px]"; 322 let link = (item["link"] ? item["link"] : ""); 323 let HTMLcontents = ""; 324 let title = processLanguage(item["title"]); 325 let cardStyle = `justify-between snap-center `; 326 let divStyle = ""; 327 let imgStyle = ` h-28 mx-auto px-12 lg:max-h-40 lg:h-auto lg:px-12 w-full lg:max-w-72`; 328 let linkNewTab = false; 329 let output = ""; 330 331 // blogs, music, events, and mobile news all use the same card template 332 if ((type === "news" && window.innerWidth < 1024) || (type === "music" || type === "blogs")) { 333 type = "events"; 334 } 335 336 if (type === "officers") { 337 type = "background"; 338 } 339 340 // ââ Freshers event card ââââââââââââââââââââââââââââââââââââââââââââââ 341 // Expects normalised event objects from the freshers microsite. 342 // Uses data-card-id for click handling via onCardClick callback. 343 if (type === "freshers-event") { 344 return freshersEventCard(item); 345 } 346
347 // ââ Background flip card âââââââââââââââââââââââââââââââââââââââââââââ 348 // Variant of the background card: same front, back content from item.back. 349 // Requires .flip-card CSS to be available on the page. 350 if (type === "background-flip") { 351 return backgroundFlipCard(item); 352 } 353 354 switch (type) { 355 case "graphic": 356 [bgColour, labelColour] = alternateColours(); 357 cardStyle = `justify-center snap-center `; 358 HTMLcontents = `<p class="text-xl lg:text-2xl font-union-black font-union-bold pt-4 mx-8 text-center text-union-${labelColour} h-1/4 lg:h-auto">${title}</p>`; 359 break; 360 361 case "descriptive": 362 HTMLcontents = `<div class="flex flex-col justify-end px-5 py-3 space-y-2 lg:h-[11rem] h-[10rem] w-full"> 363 <div class="font-union-bold text-2xl text-white leading-6">${title}</div> 364 <p class="border-t-[1.5px] pt-2 lg:text-lg text-base font-union !text-white">${desc}</p> 365 </div>`; 366 cardStyle = `justify-end snap-center `; 367 divStyle = `gradient-t-to-b-tiles-black h-full flex items-end`; 368 break; 369 370 case "background": 371 let linkCode = ``; 372 373 if (link.constructor === Array) { 374 let text = link[0]; 375 let url = link[1]; 376 link = url.trim(); 377 378 if (url.includes('@') || text.includes('@')) { 379 linkCode = ` 380 <svg class="fill-white h-6 mr-2" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 112c-8.8 0-16 7.2-16 16v22.1L220.5 291.7c20.7 17 50.4 17 71.1 0L464 150.1V128c0-8.8-7.2-16-16-16H64zM48 212.2V384c0 8.8 7.2 16 16 16H448c8.8 0 16-7.2 16-16V212.2L322 328.8c-38.4 31.5-93.7 31.5-132 0L48 212.2zM0 128C0 92.7 28.7 64 64 64H448c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"></path></svg> 381 `; 382 } 383 384 linkCode += ` 385 <p class="text-union-pink font-union-semibold text-lg text-ellipsis overflow-hidden">${processLanguage(text)}</p> 386 `; 387 } else { 388 linkCode = ` 389 <p class="text-union-pink font-union-semibold text-lg text-ellipsis overflow-hidden">${processLanguage(link)}</p> 390 `; 391 link = ``; 392 } 393 394 HTMLcontents = `<div class="flex flex-col h-fit lg:min-h-32 justify-end px-5 py-3 w-60 lg:w-80 relative bottom-0 overflow-hidden"> 395 <p class="font-union-bold text-2xl text-white w-full leading-6">${title}</p> 396 ${item["desc"] !== undefined ?'<p class="text-white font-union leading-snug text-xl w-full">'+desc+'</p>' : ""} 397 <div class="border-t-[1.5px] pt-2 mt-2 flex flex-nowrap text-ellipsis overflow-hidden w-full leading-6"> 398 ${linkCode} 399 </div> 400 </div>`; 401 cardStyle = `justify-end snap-center `; 402 divStyle = `gradient-t-to-b-tiles-black h-full flex items-end`; 403 break; 404 405 case "strand": 406 [bgColour, labelColour] = alternateColours(); 407 cardStyle = `justify-end snap-center `; 408 HTMLcontents = `<p class="bg-union-${labelColour} mb-4 lg:mb-8 text-white font-union-bold text-xl lg:text-[27px] flex rounded-r-full mr-4"> 409 <span class="my-auto lg:ml-5 mx-3 py-3 lg:py-8 font-union-bold">${title}</span> 410 </p>`; 411 break; 412 413 case "steps": 414 let hasTitle = (title === "" ? false : true); 415 divStyle = (hasTitle ? "py-8" : "py-8 lg:pt-16 lg:space-y-4"); 416 [bgColour, labelColour] = alternateColours(); 417 418 titleCode = ` 419 <p class="leading-tight flex font-union-black justify-center mx-5 text-xl lg:text-2xl text-center text-union-${labelColour} uppercase"> 420 ${title} 421 </p>` 422 HTMLcontents = `<div class="mb-2 flex flex-col h-fit ${hasTitle ? "lg:h-44" : ""} mt-3 justify-center lg:justify-end space-y-3"> 423 ${hasTitle ? titleCode : ""} 424 <p class="text-base font-union-bold h-fit ${hasTitle ? "lg:h-24" : ""} px-5 pb-5 text-center text-union-${labelColour}"> 425 ${desc} 426 </p> 427 </div>` 428 break; 429 430 case "full": 431 cardSize = `md:h-[56.25vh] xl:h-[70vh] min-w-full h-80`; 432 cardStyle=`snap-center`; 433 divStyle = `flex flex-col flex-1`; 434 HTMLcontents = `<div class="h-fit relative mt-auto p-5 lg:p-8"> 435 <div class="text-xl lg:text-5xl text-white font-union-bold"> 436 ${title} 437 </div> 438 <div class="description text-white border-t-[1.5px] pt-2"> 439 ${desc} 440 </div> 441 </div>`; 442 divStyle = `gradient-t-to-b-tiles-black h-full flex items-end`; 443 break; 444 445 case "slide": 446 cardSize = `min-w-full`; 447 cardStyle =`snap-center bg-white`; 448 divStyle = `flex lg:my-auto p-4 flex-col md:flex-row gap-4 h-full`; 449 imgStyle = `lg:w-[400px] lg:h-[200px] w-full h-[200px] mt-0 bg-center rounded-md lg:ml-8 lg:m-4 mx-auto bg-cover self-center`; 450 HTMLcontents = `<div class="${img ? "lg:pl-2" : "lg:pl-6"} w-fit lg:pr-4 lg:my-auto"> 451 <p class="text-xl font-union-semibold leading-7 lg:text-3xl ${link !== "" ? "text-union-pink" : ""}">${title}</p> 452 <p class="font-union-medium <!--lg:text-xl text-base--> lg:text-[24px] text-[17px] leading-tight mt-2 text-black">${desc}</p> 453 </div>`; 454 break; 455 456 case "advert": 457 img = item["img"].replace(/\(/g, '\\(').replace(/\)/g, '\\)'); 458 imgStyle = `w-full`; 459 [bgColour, labelColour] = alternateColours(); 460 bgColour = "white" 461 desc = processLanguage(item["description"]); 462 linkNewTab = true; 463 cardSize = `lg:min-w-80 lg:max-w-80 max-w-[288px] min-w-[288px]`; 464 HTMLcontents = `<div class="mb-2 flex flex-col h-fit mt-3 justify-center lg:justify-end space-y-3"> 465 <p class="leading-tight flex font-union-black justify-center mx-5 text-xl lg:text-2xl text-center text-union-${labelColour} uppercase"> 466 ${title} 467 </p> 468 <div class="advert-link lg:text-lg text-md font-union-medium h-fit px-5 pb-5 text-center"> 469 ${desc} 470 </div> 471 </div>` 472 break; 473 474 case "events": 475 cardStyle = `justify-end snap-center `; 476 divStyle = "p-2"; 477 let bottomText = ""; 478 const eventTitleAdditionalClasses = "tracking-normal lg:!leading-6 !leading-5 lg:!mb-4 !mb-1 !m-0 js-card-h2"; 479 480 let location = item['location']; 481 482 if (item['location'] !== undefined && typeof item['location'] !== 'string') { 483 location = (getCurrentLanguage() === 'Eng' ? item['location'][0] : item['location'][1]); 484 } 485 486 if (item['location'] != null || item['date'] != null) { 487 bottomText = `<div class="lg:flex lg:flex-row flex-col lg:items-end lg:flex-nowrap space-y-1 justify-between w-full gap-3"> 488 ${location != null ? '<p class="js-card-p lg:!leading-[1.15rem] !leading-4 font-union font-bold text-base lg:flex-1">' + location + '</p>' : ""} 489 ${item['date'] != null ? '<p class="js-card-p lg:!leading-[1.15] flex items-end font-union font-bold text-base lg:text-lg text-white">' + item['date'] + '</p>' : ""} 490 </div>` 491 } 492 493 HTMLcontents = `<div class="bg-black/60 lg:h-[8.5rem] h-[7.5rem] p-3 w-full text-white font-union-bold justify-between lg:flex flex-col rounded-xl mt-auto overflow-hidden"> 494 <div class="font-union-bold text-lg lg:text-2xl text-white w-full overflow-hidden"> 495 ${titleDecider(title, eventTitleAdditionalClasses)} 496 </div> 497 ${bottomText} 498 </div>` 499 break; 500 501 case "news": 502 if (item.tags.includes('featured') && featuredNewsBool) { 503 break; 504 } 505 506 if (pink) { 507 bgColour = 'bg-pale-pink'; 508 hoverBgColour = "#e8adca"; 509 } 510 else{ 511 bgColour = 'bg-pale-teal'; 512 hoverBgColour = "#94cad9"; 513 } 514 515 pink = !pink; 516 517 const defaultHex = bgColour === 'bg-pale-pink' ? '#F3DFEA' : '#C4DFE3'; 518 output = `<a id="${item['id']}" class="substory-card grid grid-cols-2 filter transition ease-in-out duration-300 rounded-lg ${bgColour}" href="${link}" data-hover-bg="${hoverBgColour}" data-default-bg="${defaultHex}"> 519 <div class = "${type}-card overflow-hidden rounded-l-lg"> 520 <div class="substory-image transition ease-in-out duration-300" style="background-image: url('${bgImage}'); 521 background-repeat: no-repeat;background-size: cover; 522 height: 175px;width: auto;background-position-x: center;background-position-y: center;"> 523 </div> 524 </div> 525 <div class=" newsTitle flex items-center px-5 rounded-r-lg transition ease-in-out duration-300 text-black">
526 <span class="text-xl fade font-union font-union-semibold text-black">${titleDecider(title)}</span> 527 </div> 528 </a> 529 `; 530 break; 531 532 default: 533 break; 534 } 535 536 //output for non-news cards 537 if (type !== "news") { 538 let imgContainer = `${img.length > 0 ? "<img src="+img+" loading='lazy' class='"+imgStyle+"' alt='"+alt+"'>" :""}` 539 540 if (type === "slide" && img.length > 0) { 541 imgContainer = `<div class="${imgStyle} bg-[url('${img}')]" role="img" aria-label="${alt}"></div>` 542 } 543 544 output = `<div class="js-card ${type}-card ${cardSize} bg-white rounded-xl flex flex-col ${cardStyle} ${link.length > 0 ? "grow" : ""} mx-auto ${bgColour.length > 0 ? "bg-["+bgColour+"]" : ""} ${bgImage.length > 0 ? 'bg-[url('+bgImage+')]' : ""} bg-center bg-cover"> 545 <div class="${divStyle}"> 546 ${imgContainer} 547 ${HTMLcontents} 548 </div> 549 </div>`; 550 551 if (link.length > 0) { 552 return `<a class="hover:no-underline flex ${type === "slide" || type === "full" ? "min-w-full" : "" }" href="${link}" ${linkNewTab ? 'target="_blank"' : ""}> 553 ${output} 554 </a>` 555 } 556 } 557 558 return output; 559} 560 561 562/* ââ FRESHERS EVENT CARD âââââââââââââââââââââââââââââââââââââââââââââââââââ 563 Renders a freshers microsite event card. 564 Expects a normalised event object with: id, name, type, subLabel, date, 565 startHour, venue, price, image, statuses, soldOut. 566 Uses data-card-id for click binding via onCardClick. 567 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 568 569const FRESHERS_PLACEHOLDER_IMG = 'https://www.cardiffstudents.com/assets/site_resources/placeholder-csu-image.jpg'; 570 571function freshersEventCard(ev) { 572 // These helper functions are defined in the freshers JS 573 const tc = typeClass(ev.type); 574 const colour = typeColour(ev.type); 575 const imgSrc = ev.image || FRESHERS_PLACEHOLDER_IMG; 576 const dateObj = ev.date ? new Date(ev.date + 'T00:00:00') : null; 577 const dateMonth = dateObj ? dateObj.toLocaleString('en-GB', { month: 'short' }).toUpperCase() : ''; 578 const dateDay = dateObj ? dateObj.getDate() : ''; 579 // Tours etc. are one real multi-day event, but the calendar view needs them 580 // split into a block per day -- ev.endDate carries the original span so the 581 // card (and the modal, which already does this) can show it as one range 582 // instead of looking like a duplicate one-day event. 583 const isMultiDay = !!(ev.endDate && ev.endDate !== ev.date); 584 const endDateObj = isMultiDay ? new Date(ev.endDate + 'T00:00:00') : null; 585 const endMonth = endDateObj ? endDateObj.toLocaleString('en-GB', { month: 'short' }).toUpperCase() : ''; 586 const endDay = endDateObj ? endDateObj.getDate() : ''; 587 const nonAvail = (ev.statuses || []).filter(s => s !== 'available'); 588 const isCym = typeof getCurrentLanguage === 'function' && getCurrentLanguage() === 'Cym'; 589 const badges = nonAvail.map(s => { 590 if (s.startsWith('ticket-so:')) { 591 const ticketType = s.slice(10); 592 const label = isCym ? `${ticketType} - Gwerthu Allan` : `${ticketType} - Sold Out`; 593 return `<span class="text-[0.6rem] font-union-semibold tracking-widest uppercase px-2 py-0.5 rounded-full text-white" style="background:rgba(0,0,0,0.65)">${label}</span>`; 594 } 595 const a = typeof AVAILABILITY !== 'undefined' ? AVAILABILITY[s] : null; 596 return a ? `<span class="text-[0.6rem] font-union-semibold tracking-widest uppercase px-2 py-0.5 rounded-full text-white" style="background:${a.bg}">${isCym ? a.cym : a.eng}</span>` : ''; 597 }).join(''); 598 const locationCode = (ev.venue === "" ? "" : ` 599 <div class="flex items-center gap-1.5"> 600 <img src="/pageassets/sandbox/welcome/freshers-microsite/map-pin.svg" class="h-4 rounded-none flex-shrink-0">
601 <span class="line-clamp-1">${ev.venue}</span> 602 </div> 603`); 604 // Live music (fixr/universe) events sell tickets externally, so a missing price 605 // just means no ticket-price= tag was set on the event yet -- don't default to "Free". 606 const priceDisplay = ev.yoloPricePending ? '' : (ev.price || (ev.type === 'livemusic' ? '' : 'Free')); 607 const isCymCard = typeof getCurrentLanguage === 'function' && getCurrentLanguage() === 'Cym'; 608 let btnLabel = isCymCard ? 'Cael Tocynnau' : 'Get Tickets'; 609 610 if (ev.soldOut) { 611 btnLabel = isCymCard ? 'Gwerthu Allan' : 'Sold Out'; 612 } else { 613 if (nonAvail.includes('limited')) { 614 btnLabel = 'Limited'; 615 } else if (typeof ev.tickets === "undefined") { 616 btnLabel = isCymCard ? 'Mwy o wybodaeth' : 'More info'; 617 } 618 } 619 620 return ` 621 <div class="event-card ${tc}${ev.soldOut ? ' sold-out' : ''} rounded-2xl overflow-hidden cursor-pointer bg-white transition-all hover:-translate-y-1 flex flex-col min-w-[270px] max-w-[270px] w-[270px] snap-center" data-card-id="${encodeURIComponent(ev.id)}"> 622 <div class="relative h-48 flex items-center justify-center overflow-hidden"> 623 <img src="${imgSrc}" class="w-full h-full object-cover" loading="lazy" alt="${ev.name}"> 624 ${badges ? `<div class="absolute top-2.5 right-2 z-10 flex flex-col items-end gap-1">${badges}</div>` : ''} 625 </div> 626 627 <div class="bg-white flex flex-col gap-2 p-4 rounded-b-2xl"> 628 <span class="text-[0.65rem] font-union-semibold tracking-widest px-2.5 py-0.5 rounded-full text-white w-fit" style="background:${colour}">${ev.subLabel || typeLabel(ev.type)}</span> 629 630 <div class="flex flex-1 bg-white gap-2 overflow-hidden relative"> 631 <div class="flex flex-col items-center border-black border h-fit px-1 py-0.5 rounded-lg"> 632 <span class="text-[0.6rem] font-union-semibold tracking-widest text-black">${dateMonth}</span> 633 <span class="text-xl font-union-bold text-black leading-none">${dateDay}</span> 634 ${isMultiDay ? ` 635 <span class="text-xs font-union-bold text-black leading-none">-</span> 636 <span class="text-[0.6rem] font-union-semibold tracking-widest text-black">${endMonth}</span> 637 <span class="text-xl font-union-bold text-black leading-none">${endDay}</span>` : ''} 638 </div> 639 640 <div class="flex flex-col gap-2 w-full"> 641 <div class="min-h-[3.2rem] font-union font-union-bold text-black text-xl line-clamp-2 leading-tight">${ev.name}</div> 642 643 <div class="flex w-full metadata flex gap-3 text-sm text-black items-center font-union justify-between"> 644 ${locationCode} 645 646 ${priceDisplay ? `<div class="flex items-center gap-1.5 min-w-[4.1rem]"> 647 <img src="/pageassets/sandbox/welcome/freshers-microsite/ticket.svg" class="h-4 rounded-none flex-shrink-0"> 648 ${priceDisplay === 'Free' && isCym ? 'Am Ddim' : priceDisplay} 649 </div>` : ''} 650 </div> 651 </div> 652 653 </div> 654 655 <div class="flex justify-between items-center"> 656 <button type="button" class="block card-book-btn oval-button oval-union-pink text-sm transition-colors duration-200 w-full py-2">${btnLabel}</button> 657 </div> 658 </div> 659 </div> 660 `; 661} 662 663 664/* ââ BACKGROUND FLIP CARD âââââââââââââââââââââââââââââââââââââââââââââââââ
665 Background card variant with a 3D CSS flip: same front as the background 666 card (image, title, optional desc, link text), back content from item.back. 667 Requires .flip-card / .flip-card-content / .flip-front / .flip-back CSS. 668 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 669 670function backgroundFlipCard(item) { 671 const bgImage = item['bgImage'] ? item['bgImage'].replace(/\(/g, '\\(').replace(/\)/g, '\\)') : ''; 672 const title = processLanguage(item['title']); 673 const desc = item['desc'] ? processLanguage(item['desc']) : ''; 674 const back = item['back'] ? processLanguage(item['back']) : ''; 675 let link = item['link'] || ''; 676 677 let linkCode = ''; 678 if (link.constructor === Array) { 679 const text = link[0]; 680 const url = link[1]; 681 link = url.trim(); 682 if (url.includes('@') || text.includes('@')) { 683 linkCode = `<svg class="fill-white h-6 mr-2" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M64 112c-8.8 0-16 7.2-16 16v22.1L220.5 291.7c20.7 17 50.4 17 71.1 0L464 150.1V128c0-8.8-7.2-16-16-16H64zM48 212.2V384c0 8.8 7.2 16 16 16H448c8.8 0 16-7.2 16-16V212.2L322 328.8c-38.4 31.5-93.7 31.5-132 0L48 212.2zM0 128C0 92.7 28.7 64 64 64H448c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/></svg>`; 684 } 685 linkCode += `<p class="text-union-pink font-union-semibold text-lg text-ellipsis overflow-hidden">${processLanguage(text)}</p>`; 686 } else if (link.length > 0) { 687 linkCode = `<p class="text-union-pink font-union-semibold text-lg text-ellipsis overflow-hidden">${processLanguage(link)}</p>`; 688 link = ''; 689 } 690 691 const card = `<div class="js-card background-flip-card lg:max-w-[320px] lg:h-[400px] lg:min-w-[320px] max-w-[240px] min-w-[240px] h-[300px] snap-center mx-auto"> 692 <div class="flip-card h-full"> 693 <div class="flip-card-content"> 694 <div class="flip-front ${bgImage ? 'bg-[url(' + bgImage + ')]' : ''} bg-cover bg-center bg-no-repeat"> 695 <div class="gradient-t-to-b-tiles-black h-full w-full flex items-end"> 696 <div class="flex flex-col h-fit lg:min-h-32 justify-end px-5 py-3 w-60 lg:w-80 relative bottom-0 overflow-hidden"> 697 <p class="font-union-bold text-2xl text-white w-full leading-6">${title}</p> 698 ${desc ? `<p class="text-white font-union leading-snug text-xl w-full">${desc}</p>` : ''} 699 ${linkCode ? `<div class="border-t-[1.5px] pt-2 mt-2 flex flex-nowrap text-ellipsis overflow-hidden w-full leading-6">${linkCode}</div>` : ''} 700 </div> 701 </div> 702 </div> 703 <div class="flip-back break-words p-5 overflow-y-auto"> 704 <div class="font-union text-base lg:text-lg leading-snug">${back}</div> 705 </div> 706 </div> 707 </div> 708 </div>`; 709 710 if (link.length > 0) { 711 return `<a class="hover:no-underline flex" href="${link}">${card}</a>`; 712 } 713 return card; 714} 715 716 717/** 718 * Create Title 719 */ 720function createTitle(titleText, colour){ 721 if (typeof titleText === "undefined") { 722 return ""; 723 } 724 725 if (typeof colour === "undefined") { 726 colour = "pink"; 727 } 728 729 return `<h1 class="font-union-bold text-union-${colour} text-3xl lg:text-7xl">${processLanguage(titleText)}</h1>` 730} 731 732/** 733 * Create More Button 734 */ 735function createMoreButton(buttonText, buttonLink, colour){ 736 if (typeof buttonText === "undefined" || typeof buttonLink === "undefined") { 737 return ""; 738 } 739 740 if (typeof colour === "undefined") { 741 colour = "pink"; 742 } 743 744 return ` <a href="${buttonLink}" 745 class="header-button lg:border-4 border-2 lg:py-3 py-1 font-union-bold hover:no-underline lg:text-3xl rounded-full text-center text-union-${colour} union-${colour}-button px-4 lg:px-10">${processLanguage(buttonText)} 746 </a>` 747} 748 749 750/** 751 * Get Card Data 752 */ 753async function getCardData(links, sectionType, dataArray) { 754 try { 755 const promises = links.map(async (link) => {
756 const request = new Request(link[0]); 757 const response = await fetch(request); 758 const data = await response.json(); 759 const newData = []; 760 761 for (let i = 0; i < data.length; i++) { 762 let formattedData = formatData(data[i], sectionType); 763 if (formattedData !== null) { 764 newData.push(formattedData); 765 } 766 767 if (i === link[1]) { 768 break; 769 } 770 } 771 772 return newData; 773 }); 774 775 const results = await Promise.all(promises); 776 777 const newDataArray = results.flat(); 778 779 if (newDataArray.length === 0 || !newDataArray[0].hasOwnProperty('date')) { 780 return newDataArray; 781 } 782 783 const sortedDataArray = sortDataArray(newDataArray); 784 785 return sortedDataArray; 786 } catch (error) { 787 console.error(error); 788 throw error; 789 } 790} 791 792 793/** 794 * Format Data 795 */ 796function formatData (dataItem, sectionType) { 797 var basicFields = { 798 title: (dataItem['Title'].indexOf(' | ') !== -1 ? dataItem['Title'].split(' | ') : dataItem['Title']), 799 bgImage: dataItem["ImageUrl"].replace(/\\?\?(.*)/, "")+"?thumbnail_width=320&thumbnail_height=400&resize_type=CropToFit", 800 link: dataItem["Url"], 801 }; 802 803 let location = dataItem['Location']; 804 805 if ((dataItem['Location'] !== null || dataItem['Location'] !== undefined) && dataItem['Location']?.includes(" | ")) { 806 location = dataItem['Location'].split(' | '); 807 } 808 809 var totalFields = {}; 810 811 if (sectionType === "events") { 812 let extraFields = { 813 date: dataItem["StartDate"].replace(/([T])\w(.*)/, "").split("-").reverse().join("-"), 814 location: location, 815 types: dataItem["Types"] ?? [], 816 }; 817 return totalFields = {...basicFields, ...extraFields}; 818 } 819 else if (sectionType === "music") { 820 let tempLocation = location; 821 if (tempLocation === "The Great Hall"){ 822 tempLocation = "Great Hall"; 823 } 824 let extraFields = { 825 date: dataItem["StartDate"].replace(/([T])\w(.*)/, "").split("-").reverse().join("-"), 826 location: tempLocation, 827 }; 828 829 return totalFields = {...basicFields, ...extraFields}; 830 } 831 else if (sectionType === "news"){ 832 if (dataItem['Tags'].includes("featured")){ 833 if (!featuredNewsBool) { 834 let extraFields = { 835 fullImg: dataItem["ImageUrl"].replace(/\\?\?(.*)/, ""), 836 tags: dataItem["Tags"] 837 } 838 featuredNewsBool = true; 839 return totalFields = {...basicFields, ...extraFields}; 840 } 841 else{ 842 return null; 843 } 844 } 845 else { 846 let extraFields = { 847 tags: "", 848 id: "news"+newsId 849 } 850 newsId++; 851 return totalFields = {...basicFields, ...extraFields}; 852 } 853 } else if (sectionType === "flipcard") { 854 let extraFields = { 855 date: dataItem["StartDate"].replace(/([T])\w(.*)/, "").split("-").reverse().join("-"), 856 endDate: dataItem["EndDate"].replace(/([T])\w(.*)/, "").split("-").reverse().join("-"), 857 startTime: dataItem["StartDate"].split("T")[1].substring(0, 5), 858 endTime: dataItem["EndDate"].split("T")[1].substring(0, 5), 859 location: location, 860 types: dataItem["Types"], 861 description: dataItem["Description"], 862 hasTickets: dataItem["HasTickets"], 863 tickets: dataItem["Tickets"], 864 img: dataItem["ImageUrl"].replace(/\\?\?(.*)/, ""), 865 longDesc: dataItem["Body"], 866 }; 867 868 return totalFields = {...basicFields, ...extraFields}; 869 } else if (sectionType === "advert") { 870 let desc = (dataItem["Description"] ? dataItem["Description"] : dataItem["Body"]) 871 let extraFields = { 872 description: desc, 873 img: dataItem["ImageUrl"].replace(/\\?\?(.*)/, ""), 874 }; 875 876 return totalFields = {...basicFields, ...extraFields}; 877 } 878 else { 879 return basicFields; 880 } 881} 882 883 884/** 885 * Sort Data Array 886 */ 887function sortDataArray(dataArray) { 888 dataArray.sort(function(a, b) { 889 const dateA = parseCustomDate(a.date); 890 const dateB = parseCustomDate(b.date); 891 return dateA - dateB; 892 });
893 894 return dataArray; 895} 896 897 898function parseCustomDate(dateStr) { 899 const [day, month, year] = dateStr.split('-'); 900 return new Date(`${year}-${month}-${day}`).getTime(); 901} 902 903 904/** 905 * Title Decider 906 */ 907function titleDecider(title, typeTitleAdditionalClasses = "") { 908 const titleClass = `${typeTitleAdditionalClasses} h-full overflow-hidden text-ellipsis font-union-semibold js-card-h2`; 909 let newTitle = `<h2 class="${titleClass}">${processLanguage(title)}</h2>`; 910 911 return newTitle; 912} 913 914 915// Delegated hover handlers for news cards â replaces inline onMouseOver/onMouseOut 916$(document).on('mouseenter', '.substory-card', function () { 917 $(this).css('background-color', $(this).data('hover-bg')); 918}).on('mouseleave', '.substory-card', function () { 919 $(this).css('background-color', $(this).data('default-bg')); 920}); 921 922 923/** 924 * Stop Loading 925 */ 926function stopLoading(){ 927 $('#loadingBarEventsMobile').addClass('hidden'); 928 $('#loadingBarEvents').addClass('hidden'); 929} 930 931 932/* ââ FLIP CARD TOUCH SUPPORT âââââââââââââââââââââââââââââââââââââââââââââ 933 CSS :hover doesn't fire reliably on touch devices. On touch, we toggle a 934 .flipped class instead. The CSS counterpart disables :hover on touch and 935 mirrors the transform onto .flipped â see flip card stylesheet. 936 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 937$(document).on('click', '.flip-card', function (e) { 938 if (!window.matchMedia('(hover: none) and (pointer: coarse)').matches) return; 939 if ($(e.target).closest('a[href]').length) return; 940 $(this).find('.flip-card-content').toggleClass('card-is-flipped'); 941});
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.