github.com/timo-reymann/yal@v0.0.0-20240419173834-5d47db58f9d1/pkg/templating/index.gohtml (about) 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <title>{{ .PageTitle }}</title> 5 6 <meta charset="UTF-8"> 7 <meta name='generator' content='YAL {{ Version }}'> 8 <meta name='robots' content='noindex,nofollow'> 9 <meta name='apple-mobile-web-app-status-bar-style' content='black'> 10 11 <link rel="icon" href="{{ .Favicon }}"> 12 <style> 13 * { 14 margin: 0; 15 padding: 0; 16 } 17 18 html, body { 19 min-height: 100vh; 20 position: relative; 21 } 22 23 html { 24 color: white; 25 background: black; 26 background-image: url("{{.Background}}"); 27 background-repeat: no-repeat; 28 background-attachment: fixed; 29 background-position: center; 30 background-size: cover; 31 } 32 33 body { 34 font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 35 backdrop-filter: {{.Assets.BackgroundFilter}}; 36 } 37 38 header { 39 position: relative; 40 padding-top: 30px; 41 } 42 43 main { 44 width: 50vw; 45 margin: auto; 46 padding-bottom: 100px; 47 } 48 49 .overlay--left, 50 .overlay--right { 51 position: absolute; 52 top: 0; 53 height: 100%; 54 z-index: -1; 55 } 56 57 .overlay--left { 58 left: 0; 59 } 60 61 .overlay--right { 62 right: 0; 63 } 64 65 .search { 66 width: 50vw; 67 margin: auto; 68 position: relative; 69 } 70 71 .search--icon-overlay { 72 position: absolute; 73 width: 32px; 74 height: 32px; 75 left: 20px; 76 top: 0; 77 bottom: 0; 78 margin: auto 0; 79 vertical-align: middle; 80 } 81 82 .search--search-field { 83 border: .2em solid rgb(63, 68, 70); 84 background-color: rgba(25, 26, 27,.6); 85 color: white; 86 border-radius: 20px; 87 box-shadow: 0 1px 5px rgba(0, 0, 0, .3); 88 background-repeat: no-repeat; 89 background-attachment: fixed; 90 background-size: 24px; 91 border-bottom-width: 3px; 92 text-align: left; 93 font-size: 1.414rem; 94 outline: none; 95 width: 100%; 96 padding: 15px 15px 15px 60px; 97 } 98 99 .search-suggestions { 100 border: .2em solid rgb(63, 68, 70); 101 box-shadow: 0 1px 5px rgba(0, 0, 0, .3); 102 cursor: auto; 103 background-color: rgb(25, 26, 27,1); 104 backdrop-filter: blur(10px); 105 border-top: 1px solid rgb(63, 68, 70); 106 display: block; 107 font-size: 15px; 108 text-align: left; 109 border-radius: 0 0 20px 20px; 110 margin-top: -5px; 111 margin-left: 10px; 112 margin-right: 10px; 113 position: absolute; 114 left: 0; 115 right: 0; 116 } 117 118 .search-suggestion:first-child { 119 padding: 5px; 120 } 121 122 .search-suggestion--item { 123 color: inherit; 124 padding: 10px 5px; 125 transition: all .1s; 126 display: flex; 127 align-items: center; 128 text-decoration: none; 129 } 130 131 .search-suggestion--item-title { 132 font-weight: bold; 133 } 134 135 .search-suggestion--item-description { 136 text-align: right; 137 font-style: italic; 138 } 139 140 .search-suggestion--item-icon { 141 display: inline-block; 142 vertical-align: middle; 143 width: 32px; 144 height: 32px; 145 padding: 4px 10px 4px 4px; 146 } 147 148 .search-suggestion--item:last-child:hover { 149 border-bottom-left-radius: 17px; 150 border-bottom-right-radius: 17px; 151 } 152 153 .search-suggestion--item:hover, 154 .search-suggestion--item[data-active] { 155 background-color: rgba(51, 118, 184,.5); 156 color: white; 157 } 158 159 .search-suggestion--item-content { 160 display: flex; 161 flex-grow: 1; 162 gap: 10px; 163 } 164 165 .search-suggestion--item-seperator { 166 flex: 1; 167 } 168 169 .search-suggestion--item-description { 170 padding-right: 20px; 171 text-align: left; 172 min-width: 50%; 173 } 174 175 .item-section { 176 display: flex; 177 flex-direction: column; 178 justify-content: start; 179 align-items: center; 180 } 181 182 .items-container--no-results { 183 text-align: center; 184 font-size: 20px; 185 margin: 20px; 186 } 187 188 .item-section--items { 189 display: flex; 190 align-items: flex-start; 191 flex-wrap: wrap; 192 justify-content: center; 193 gap: 25px; 194 } 195 196 .item-section--item { 197 width: 180px; 198 height: 180px; 199 color: white; 200 display: flex; 201 flex-direction: column; 202 justify-content: center; 203 align-items: center; 204 text-decoration: none; 205 border-radius: 10%; 206 padding: 10px; 207 } 208 209 .item-section--item:hover { 210 background-color: hsla(0, 0%, 100%, .082); 211 transition: background-color .1s; 212 } 213 214 .item-section--title { 215 margin-top: 60px; 216 margin-bottom: 20px; 217 font-size: 2rem; 218 } 219 220 .item-section--item-icon { 221 display: inline-block; 222 background-color: rgba(25, 26, 27, 0.6); 223 padding: 20px; 224 max-width: 100%; 225 min-height: 100px; 226 max-height: 60%; 227 box-shadow: 0 1px 5px rgba(0, 0, 0, .3); 228 border-radius: 20%; 229 } 230 231 .item-section--item-title { 232 font-weight: bold; 233 margin-top: 10px; 234 text-align: center; 235 min-height: 30px; 236 } 237 238 .overlay--logo, 239 .overlay--mascot { 240 position: sticky; 241 z-index: -1; 242 bottom: 0; 243 } 244 245 .overlay--logo { 246 top: 80vh; 247 margin-right: 60px; 248 max-width: 200px; 249 max-height: 130px; 250 } 251 252 .overlay--mascot { 253 top: 30vh; 254 padding-left: 20px; 255 max-width: 200px; 256 } 257 258 .hidden { 259 display: none; 260 } 261 262 @media(max-width: 530px) { 263 .overlay--mascot, 264 .overlay--logo { 265 display: none; 266 } 267 } 268 269 @media(max-width: 900px) { 270 .overlay--mascot, 271 .overlay--logo { 272 top: 5%; 273 max-width: 8vw; 274 } 275 276 main { 277 width: 80vw; 278 } 279 } 280 281 @media(min-width: 2000px) { 282 main { 283 width: 80vw; 284 } 285 } 286 </style> 287 <script> 288 const searchEngines = [ 289 {{ range .SearchEngines }} 290 { 291 title: "{{ .Title }}", 292 target_prefix: "{{ .UrlPrefix }}", 293 }, 294 {{ end }} 295 ]; 296 297 document.addEventListener("DOMContentLoaded", () => { 298 const searchContainer = document.querySelector(".search"); 299 const searchField = document.querySelector(".search--search-field"); 300 const searchResults = document.querySelector(".search-suggestions"); 301 const itemsContainer = document.querySelector(".items-container"); 302 const noItemsContainer = document.querySelector(".items-container--no-results"); 303 304 let activeItem; 305 306 const createSearchSuggestionItem = (title, description, target, icon, active = false) => { 307 return `<a href="${target}" role="menuitem" 308 ${active ? 'data-active' : ''} 309 class="search-suggestion--item"> 310 <img class="search-suggestion--item-icon" alt="${title}" src="${icon}" /> 311 <div class="search-suggestion--item-content"> 312 <span class="search-suggestion--item-title">${title}</span> 313 <span class="search-suggestion--item-seperator"></span> 314 <span class="search-suggestion--item-description">${description}</span> 315 </div> 316 </a>` 317 } 318 319 const renderSearchSuggestions = (itemsToDisplay) => { 320 searchResults.innerHTML = ""; 321 for (let i = 0; i < itemsToDisplay.length; i++) { 322 const itemToDisplay = itemsToDisplay[i]; 323 searchResults.innerHTML += createSearchSuggestionItem( 324 itemToDisplay.title, 325 itemToDisplay.description, 326 itemToDisplay.target, 327 itemToDisplay.icon, 328 i === 0, 329 ) 330 } 331 } 332 333 const setSearchResultsVisible = (state) => { 334 if (state) { 335 searchResults.classList.remove("hidden"); 336 } else { 337 searchResults.classList.add("hidden"); 338 setActiveNode(searchResults.children[0]) 339 } 340 }; 341 342 const setActiveNode = (nodeToActivate) => { 343 if (activeItem) { 344 delete activeItem.dataset.active; 345 } 346 if (nodeToActivate) { 347 nodeToActivate.dataset.active = ""; 348 } 349 }; 350 351 searchField.addEventListener("blur", () => { 352 setTimeout(() => setSearchResultsVisible(false), 100); 353 }) 354 searchField.addEventListener("focus", () => setSearchResultsVisible(true)); 355 356 searchField.addEventListener("input", () => { 357 const searchResults = [] 358 359 for (const item of Array.from(itemsContainer.querySelectorAll("[data-search-text]"))) { 360 const matchesSearch = item.dataset.searchText.toLowerCase().includes(searchField.value.toLowerCase()); 361 362 if (matchesSearch) { 363 searchResults.push({ 364 title: item.text, 365 description: item.dataset.description, 366 target: item.href, 367 icon: item.querySelector(".item-section--item-icon").src, 368 }); 369 item.classList.remove("hidden"); 370 } else { 371 item.classList.add("hidden"); 372 } 373 } 374 375 let anyVisibleSection = false; 376 for(const section of Array.from(itemsContainer.querySelectorAll(".item-section"))) { 377 const sectionHasVisibleItems = Array.from(section.querySelectorAll(".item-section--item")) 378 .filter(el => !el.classList.contains("hidden")) 379 .length > 0; 380 if(sectionHasVisibleItems) { 381 section.classList.remove("hidden"); 382 anyVisibleSection = true; 383 } else { 384 section.classList.add("hidden"); 385 } 386 } 387 388 if(anyVisibleSection) { 389 noItemsContainer.classList.add("hidden"); 390 } else { 391 noItemsContainer.classList.remove("hidden"); 392 } 393 394 searchResults.sort((a, b) => a.title.localeCompare(b.title)) 395 396 const displayResults = searchResults.slice(0, 5) 397 398 for (const searchEngine of searchEngines) { 399 displayResults.push({ 400 title: searchEngine.title, 401 description: `Search in ${searchEngine.title}`, 402 target: searchEngine.target_prefix + searchField.value, 403 icon: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjZmZmIiBoZWlnaHQ9IjgwMHB4IiB3aWR0aD0iODAwcHgiIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgdmlld0JveD0iMCAwIDQ4OC40IDQ4OC40IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNMCwyMDMuMjVjMCwxMTIuMSw5MS4yLDIwMy4yLDIwMy4yLDIwMy4yYzUxLjYsMCw5OC44LTE5LjQsMTM0LjctNTEuMmwxMjkuNSwxMjkuNWMyLjQsMi40LDUuNSwzLjYsOC43LDMuNiAgICBzNi4zLTEuMiw4LjctMy42YzQuOC00LjgsNC44LTEyLjUsMC0xNy4zbC0xMjkuNi0xMjkuNWMzMS44LTM1LjksNTEuMi04Myw1MS4yLTEzNC43YzAtMTEyLjEtOTEuMi0yMDMuMi0yMDMuMi0yMDMuMiAgICBTMCw5MS4xNSwwLDIwMy4yNXogTTM4MS45LDIwMy4yNWMwLDk4LjUtODAuMiwxNzguNy0xNzguNywxNzguN3MtMTc4LjctODAuMi0xNzguNy0xNzguN3M4MC4yLTE3OC43LDE3OC43LTE3OC43ICAgIFMzODEuOSwxMDQuNjUsMzgxLjksMjAzLjI1eiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo=", 404 }); 405 } 406 407 renderSearchSuggestions(displayResults); 408 setSearchResultsVisible(searchField.value.trim() !== ""); 409 }); 410 411 searchContainer.addEventListener("keyup", e => { 412 activeItem = document.querySelector("[data-active]"); 413 switch (e.key) { 414 case "ArrowDown": 415 setActiveNode(activeItem.nextElementSibling || searchResults.children[0]); 416 break; 417 418 case "ArrowUp": 419 setActiveNode(activeItem.previousElementSibling || searchResults.children[searchResults.children.length - 1]); 420 break; 421 422 case "Enter": 423 setSearchResultsVisible(false); 424 activeItem.click(); 425 break; 426 } 427 }); 428 }); 429 430 </script> 431 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 432 </head> 433 <body> 434 435 <header role="presentation"> 436 <div class="search" role="presentation" > 437 <img aria-hidden="true" alt="Search" class="search--icon-overlay" 438 src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjZmZmIiBoZWlnaHQ9IjgwMHB4IiB3aWR0aD0iODAwcHgiIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgdmlld0JveD0iMCAwIDQ4OC40IDQ4OC40IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNMCwyMDMuMjVjMCwxMTIuMSw5MS4yLDIwMy4yLDIwMy4yLDIwMy4yYzUxLjYsMCw5OC44LTE5LjQsMTM0LjctNTEuMmwxMjkuNSwxMjkuNWMyLjQsMi40LDUuNSwzLjYsOC43LDMuNiAgICBzNi4zLTEuMiw4LjctMy42YzQuOC00LjgsNC44LTEyLjUsMC0xNy4zbC0xMjkuNi0xMjkuNWMzMS44LTM1LjksNTEuMi04Myw1MS4yLTEzNC43YzAtMTEyLjEtOTEuMi0yMDMuMi0yMDMuMi0yMDMuMiAgICBTMCw5MS4xNSwwLDIwMy4yNXogTTM4MS45LDIwMy4yNWMwLDk4LjUtODAuMiwxNzguNy0xNzguNywxNzguN3MtMTc4LjctODAuMi0xNzguNy0xNzguN3M4MC4yLTE3OC43LDE3OC43LTE3OC43ICAgIFMzODEuOSwxMDQuNjUsMzgxLjksMjAzLjI1eiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="/> 439 440 <input type="search" placeholder="Search ..." class="search--search-field"/> 441 <div role="menu" aria-label="Search results" class="search-suggestions hidden"> 442 </div> 443 </div> 444 </header> 445 <aside class="overlay--left" role="presentation" aria-description="Mascot"> 446 <img class="overlay--mascot" src="{{ .Mascot }}" alt="Mascot"/> 447 </aside> 448 <aside class="overlay--right" role="presentation" aria-description="Logo"> 449 <img class="overlay--logo" src="{{ .Logo }}" alt="Logo"/> 450 </aside> 451 <main class="items-container"> 452 <p class="items-container--no-results hidden"> 453 No items to display. 454 </p> 455 {{ range .Sections }} 456 <section role="group" class="item-section" aria-label="{{ .Title }}"> 457 <h2 aria-hidden="true" role="heading" class="item-section--title">{{ .Title }}</h2> 458 <div class="item-section--items"> 459 {{ range .Entries }} 460 <a class="item-section--item" href="{{ .Link }}" data-search-text="{{ .Text }}" data-description="{{ .Description }}" title="{{ .Description }}" aria-label="{{ .Text }}" aria-description="{{ .Description }}" role="listitem"> 461 <img alt="icon" 462 aria-hidden="true" 463 src="{{ .InlineIcon }}" 464 class="item-section--item-icon"></img> 465 <div class="item-section--item-title">{{ .Text }}</div> 466 </a> 467 {{end}} 468 </div> 469 </section> 470 {{ end }} 471 </main> 472 </body> 473 </html>