Refactor home.html: Update navbar structure, enhance styling, and implement search functionality
- Working infinite scroll effect - Removed unused CSS and JS files to streamline the template.
|
|
@ -4,7 +4,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
import com.example.PhotoGallery.Services.PhotoService;
|
import com.example.PhotoGallery.Services.PhotoService;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
|
@ -15,23 +14,36 @@ public class PhotosController {
|
||||||
|
|
||||||
private final int RANDOM_NUMBER_IMAGES = 5;
|
private final int RANDOM_NUMBER_IMAGES = 5;
|
||||||
private final int FIRST_PAGE = 0;
|
private final int FIRST_PAGE = 0;
|
||||||
private final int INITIAL_PAGE_SIZE = 30;
|
private final int PAGE_SIZE = 10;
|
||||||
|
private int pageCounter = 0;
|
||||||
|
|
||||||
@GetMapping("/home")
|
@GetMapping("/home")
|
||||||
private String home(Model model) {
|
private String home(Model model) {
|
||||||
|
|
||||||
photoService.WriteImagesFromDirToDB();
|
//reset the page counter when the page home page is reloaded
|
||||||
|
pageCounter = 0;
|
||||||
|
|
||||||
model.addAttribute("images", photoService.getPagedPhotos(FIRST_PAGE, INITIAL_PAGE_SIZE));
|
//upon a page refresh we will scan the db and add any new images that are in the directory
|
||||||
|
photoService.WriteImagesFromDirToDB(); //TODO: run this method asynchronously
|
||||||
|
|
||||||
|
model.addAttribute("images", photoService.getPagedPhotos(FIRST_PAGE, PAGE_SIZE));
|
||||||
model.addAttribute("carouselImages", photoService.getRandomPhotos(RANDOM_NUMBER_IMAGES));
|
model.addAttribute("carouselImages", photoService.getRandomPhotos(RANDOM_NUMBER_IMAGES));
|
||||||
|
|
||||||
return "home";
|
return "home";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/images")
|
@GetMapping("/loadMoreImages")
|
||||||
public String test(Model model) {
|
public String loadMoreImages(Model model) {
|
||||||
model.addAttribute("images", photoService.getPagedPhotos(0, 10));
|
int totalPages = photoService.getPagedPhotos(FIRST_PAGE, PAGE_SIZE).getTotalPages();
|
||||||
|
|
||||||
|
//dont load any more images if we have reached the last images
|
||||||
|
if (pageCounter == totalPages){
|
||||||
|
return "home :: images";
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCounter++;
|
||||||
|
model.addAttribute("images", photoService.getPagedPhotos(pageCounter, 10));
|
||||||
return "home :: images";
|
return "home :: images";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
/* !
|
|
||||||
* baguetteBox.js
|
|
||||||
* @author feimosi
|
|
||||||
* @version 1.11.1
|
|
||||||
* @url https://github.com/feimosi/baguetteBox.js */
|
|
||||||
|
|
||||||
#baguetteBox-overlay {
|
|
||||||
display: none;
|
|
||||||
opacity: 0;
|
|
||||||
position: fixed;
|
|
||||||
overflow: hidden;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1000000;
|
|
||||||
background-color: #222;
|
|
||||||
background-color: rgba(0,0,0,.8);
|
|
||||||
-webkit-transition: opacity .5s ease;
|
|
||||||
transition: opacity .5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-overlay.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-overlay .full-image {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-overlay .full-image figure {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-overlay .full-image img {
|
|
||||||
display: inline-block;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
vertical-align: middle;
|
|
||||||
-webkit-box-shadow: 0 0 8px rgba(0,0,0,.6);
|
|
||||||
-moz-box-shadow: 0 0 8px rgba(0,0,0,.6);
|
|
||||||
box-shadow: 0 0 8px rgba(0,0,0,.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-overlay .full-image figcaption {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.8;
|
|
||||||
white-space: normal;
|
|
||||||
color: #ccc;
|
|
||||||
background-color: #000;
|
|
||||||
background-color: rgba(0,0,0,.6);
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-overlay .full-image:before {
|
|
||||||
content: "";
|
|
||||||
display: inline-block;
|
|
||||||
height: 50%;
|
|
||||||
width: 1px;
|
|
||||||
margin-right: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-slider {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
-webkit-transition: left .4s ease,-webkit-transform .4s ease;
|
|
||||||
transition: left .4s ease,-webkit-transform .4s ease;
|
|
||||||
transition: left .4s ease,transform .4s ease;
|
|
||||||
transition: left .4s ease,transform .4s ease,-webkit-transform .4s ease,-moz-transform .4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-slider.bounce-from-right {
|
|
||||||
-webkit-animation: bounceFromRight .4s ease-out;
|
|
||||||
animation: bounceFromRight .4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#baguetteBox-slider.bounce-from-left {
|
|
||||||
-webkit-animation: bounceFromLeft .4s ease-out;
|
|
||||||
animation: bounceFromLeft .4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounceFromRight {
|
|
||||||
0%, 100% {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
margin-left: -30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounceFromLeft {
|
|
||||||
0%, 100% {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button#next-button, .baguetteBox-button#previous-button {
|
|
||||||
top: 50%;
|
|
||||||
top: calc(50% - 30px);
|
|
||||||
width: 44px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: 0;
|
|
||||||
-moz-border-radius: 15%;
|
|
||||||
border-radius: 15%;
|
|
||||||
background-color: #323232;
|
|
||||||
background-color: rgba(50,50,50,.5);
|
|
||||||
color: #ddd;
|
|
||||||
font: 1.6em sans-serif;
|
|
||||||
-webkit-transition: background-color .4s ease;
|
|
||||||
transition: background-color .4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button:focus, .baguetteBox-button:hover {
|
|
||||||
background-color: rgba(50,50,50,.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button#next-button {
|
|
||||||
right: 2%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button#previous-button {
|
|
||||||
left: 2%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button#close-button {
|
|
||||||
top: 20px;
|
|
||||||
right: 2%;
|
|
||||||
right: calc(2% + 6px);
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-button svg {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -20px;
|
|
||||||
margin-left: -20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-double-bounce1, .baguetteBox-double-bounce2 {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
-moz-border-radius: 50%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fff;
|
|
||||||
opacity: .6;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
-webkit-animation: bounce 2s infinite ease-in-out;
|
|
||||||
animation: bounce 2s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.baguetteBox-double-bounce2 {
|
|
||||||
-webkit-animation-delay: -1s;
|
|
||||||
animation-delay: -1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0%, 100% {
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-moz-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
-moz-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
.bs-icon {
|
|
||||||
--bs-icon-size: .75rem;
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--bs-icon-size);
|
|
||||||
width: calc(var(--bs-icon-size) * 2);
|
|
||||||
height: calc(var(--bs-icon-size) * 2);
|
|
||||||
color: var(--bs-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon-xs {
|
|
||||||
--bs-icon-size: 1rem;
|
|
||||||
width: calc(var(--bs-icon-size) * 1.5);
|
|
||||||
height: calc(var(--bs-icon-size) * 1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon-sm {
|
|
||||||
--bs-icon-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon-md {
|
|
||||||
--bs-icon-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon-lg {
|
|
||||||
--bs-icon-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon-xl {
|
|
||||||
--bs-icon-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon.bs-icon-primary {
|
|
||||||
color: var(--bs-white);
|
|
||||||
background: var(--bs-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon.bs-icon-primary-light {
|
|
||||||
color: var(--bs-primary);
|
|
||||||
background: rgba(var(--bs-primary-rgb), .2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon.bs-icon-semi-white {
|
|
||||||
color: var(--bs-primary);
|
|
||||||
background: rgba(255, 255, 255, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon.bs-icon-rounded {
|
|
||||||
border-radius: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-icon.bs-icon-circle {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
.aspect-ratio-4x3 {
|
|
||||||
aspect-ratio: 4/3;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
|
@ -1,3 +0,0 @@
|
||||||
if (document.querySelectorAll('[data-bss-baguettebox]').length > 0) {
|
|
||||||
baguetteBox.run('[data-bss-baguettebox]', { animation: 'slideIn' });
|
|
||||||
}
|
|
||||||
|
|
@ -6,44 +6,46 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
<title>Untitled</title>
|
<title>Untitled</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="/css/bss-overrides.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="/css/Lightbox-Gallery-baguetteBox.min.css">
|
|
||||||
<link rel="stylesheet" href="/css/Navbar-Centered-Links-icons.css">
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #222831;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search:active{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="height: 100%;width: 100%;">
|
<body style="height: 100%;width: 100%;">
|
||||||
<nav class="navbar navbar-expand-md bg-body mb-3 py-3">
|
<nav class="navbar navbar-expand-md sticky-top bg-body border-bottom">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand d-flex align-items-center" href="#">
|
<a class="navbar-brand fw-bold" href="#">Photo Gallery</a>
|
||||||
<span
|
|
||||||
class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor"
|
|
||||||
viewBox="0 0 16 16" class="bi bi-camera">
|
|
||||||
<path
|
|
||||||
d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z">
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>Photo Gallery</span>
|
|
||||||
</a>
|
|
||||||
<button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-3">
|
|
||||||
<span class="visually-hidden">Toggle navigation</span>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navcol-3">
|
|
||||||
<ul class="navbar-nav align-items-center mx-auto">
|
<div class="collapse navbar-collapse" id="navMain">
|
||||||
|
<ul class="navbar-nav ms-auto align-items-md-center gap-md-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active my-0 py-2" th:href="@{/uploadPhoto}">First Item</a>
|
<div>
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
<input style="border: none;" type="search" name="search" id="search" placeholder="Search Photos...">
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#">Second Item</a>
|
<button class="btn btn-outline-dark dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
</li>
|
Dropdown
|
||||||
<li class="nav-item">
|
</button>
|
||||||
<a class="nav-link" href="#">Third Item</a>
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
|
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,39 +79,28 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<section class="pt-5 pb-0 my-0 py-xl-5" style="padding: 0;">
|
<section class="pt-5 pb-0 my-0 py-xl-5" style="padding: 0;">
|
||||||
<div class="d-flex">
|
|
||||||
<div class="w-100">
|
|
||||||
<input class="mx-2 px-2 my-3 py-1" type="search" placeholder="Search Photos...">
|
|
||||||
</div>
|
|
||||||
<div class="d-flex w-100 align-items-center justify-content-end ">
|
|
||||||
<select class="p-2 me-3" name="" id="">
|
|
||||||
<option value="">People</option>
|
|
||||||
<option value="">Landscapes</option>
|
|
||||||
<option value="">Animals</option>
|
|
||||||
<option value="">Black & White</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div th:fragment="images" class="row gx-2 gy-2 row-cols-1 row-cols-md-2 row-cols-xl-3 mt-0"
|
<div id="image-row" class="row gx-1 gy-1 row-cols-1 row-cols-md-2 row-cols-xl-5 mt-0"
|
||||||
data-bss-baguettebox="">
|
data-bss-baguettebox="">
|
||||||
|
|
||||||
<div class="col" th:each="image : ${images}">
|
<div th:fragment="images" class="col" th:each="image : ${images}">
|
||||||
|
<span th:text="${images.number}"></span>
|
||||||
<a th:href="@{${image.filePath}}">
|
<a th:href="@{${image.filePath}}">
|
||||||
<img class="img-fluid aspect-ratio-4x3 object-fit-cover w-100 h-100"
|
<img class="bg-black img-fluid aspect-ratio-4x3 object-fit-cover w-100 h-100"
|
||||||
|
style="padding: 1px;"
|
||||||
alt="Replace with image description" th:src="@{${image.thumbnailPath}}"
|
alt="Replace with image description" th:src="@{${image.thumbnailPath}}"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTMX infinite-scroll sentinel -->
|
|
||||||
<div th:if="${images}" hx-get="/images" hx-trigger="revealed"
|
|
||||||
hx-swap="afterend" style="height: 1px">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="height: 1px;"
|
||||||
|
hx-get="/loadMoreImages"
|
||||||
|
hx-trigger="intersect"
|
||||||
|
hx-target="#image-row"
|
||||||
|
hx-swap="beforeend">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -117,8 +108,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/js/Lightbox-Gallery-baguetteBox.min.js"></script>
|
|
||||||
<script src="/js/Lightbox-Gallery.js"></script>
|
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||