@page
@model IndexModel
@{
ViewData["Title"] = "Gallery";
}
<div class="d-flex flex-wrap gap-2 align-items-end mt-3">
<form method="post" asp-page-handler="Update" class="mb-0">
<button type="submit" class="btn btn-success">Update</button>
</form>
<form method="get" class="row g-2 align-items-end flex-grow-1 mb-0">
<div class="col-12 col-sm-6 col-md-4">
<input id="keywordInput"
name="keyword"
class="form-control"
value="@Model.Keyword"
placeholder="Search by keyword"
aria-label="Keyword" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">Search</button>
<a class="btn btn-outline-secondary" href="@Url.Page("Index")">Clear</a>
</div>
</form>
</div>
@if (!string.IsNullOrWhiteSpace(Model.StatusMessage))
{
<div class="alert alert-info mt-3">@Model.StatusMessage</div>
}
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-6 g-3 mt-3">
@if (Model.Folders.Count == 0)
{
<div class="col">
<div class="alert alert-secondary mb-0">No records yet.</div>
</div>
}
else
{
foreach (var card in Model.Folders)
{
var item = card.Folder;
<div class="col">
<button type="button"
class="card h-100 text-start shadow-sm folder-card"
data-folder-id="@item.Id"
data-title="@item.Title"
data-date="@item.Date.ToString("yyyy-MM-dd")"
data-author="@item.Author"
data-count="@(item.FileCount ?? 0)"
data-keywords="@item.Keywords">
@if (!string.IsNullOrWhiteSpace(card.CoverUrl))
{
<img src="@card.CoverUrl" alt="@card.CoverFileName" class="card-img-top" />
}
<div class="card-body d-flex flex-column">
<h5 class="card-title mb-1">@item.Title</h5>
<div class="text-muted small">@item.Date.ToString("yyyy-MM-dd") · @item.Author</div>
@if (!string.IsNullOrWhiteSpace(item.Keywords))
{
<div class="mt-2 text-body-secondary small">@item.Keywords</div>
}
<div class="mt-auto pt-2">
<span class="badge text-bg-light">@((item.FileCount ?? 0).ToString()) files</span>
</div>
</div>
</button>
</div>
}
}
</div>
@if (Model.TotalPages > 1)
{
<nav class="mt-3" aria-label="Pagination">
<ul class="pagination mb-0">
<li class="page-item @(Model.PageNumber <= 1 ? "disabled" : "")">
<a class="page-link"
href="@Url.Page("Index", null, new { page = Model.PageNumber - 1, keyword = Model.Keyword })"
aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
@for (var i = 1; i <= Model.TotalPages; i++)
{
<li class="page-item @(i == Model.PageNumber ? "active" : "")">
<a class="page-link" href="@Url.Page("Index", null, new { page = i, keyword = Model.Keyword })">@i</a>
</li>
}
<li class="page-item @(Model.PageNumber >= Model.TotalPages ? "disabled" : "")">
<a class="page-link"
href="@Url.Page("Index", null, new { page = Model.PageNumber + 1, keyword = Model.Keyword })"
aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
}
<div class="modal fade" id="folderModal" tabindex="-1" aria-labelledby="folderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="folderModalLabel">Files</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="folderMeta" class="mb-2 text-muted small"></div>
<div id="folderKeywords" class="mb-2"></div>
<div id="folderLoading" class="alert alert-light border">Loading...</div>
<div id="folderEmpty" class="alert alert-secondary d-none">No images found.</div>
<div id="folderImages" class="row g-2"></div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
const modalEl = document.getElementById("folderModal");
const folderModal = new bootstrap.Modal(modalEl);
const titleEl = document.getElementById("folderModalLabel");
const metaEl = document.getElementById("folderMeta");
const keywordsEl = document.getElementById("folderKeywords");
const loadingEl = document.getElementById("folderLoading");
const emptyEl = document.getElementById("folderEmpty");
const imagesEl = document.getElementById("folderImages");
function setLoading(isLoading) {
loadingEl.classList.toggle("d-none", !isLoading);
}
function setEmpty(isEmpty) {
emptyEl.classList.toggle("d-none", !isEmpty);
}
function clearImages() {
imagesEl.innerHTML = "";
}
function renderKeywords(raw) {
keywordsEl.innerHTML = "";
if (!raw) {
return;
}
const list = raw.split(",").map(k => k.trim()).filter(k => k.length > 0);
if (list.length === 0) {
return;
}
const label = document.createElement("span");
label.className = "text-muted small me-2";
label.textContent = "Keywords:";
keywordsEl.appendChild(label);
for (const keyword of list) {
const link = document.createElement("a");
link.href = `?keyword=${encodeURIComponent(keyword)}`;
link.className = "badge text-bg-light me-1 text-decoration-none";
link.textContent = keyword;
keywordsEl.appendChild(link);
}
}
async function loadImages(folderId) {
setLoading(true);
setEmpty(false);
clearImages();
try {
const response = await fetch(`?handler=Images&id=${encodeURIComponent(folderId)}`);
if (!response.ok) {
setEmpty(true);
return;
}
const files = await response.json();
if (!files || files.length === 0) {
setEmpty(true);
return;
}
for (const file of files) {
const col = document.createElement("div");
col.className = "col-6 col-md-4 col-lg-3";
const img = document.createElement("img");
img.src = file.url;
img.alt = file.fileName || "image";
img.loading = "lazy";
img.className = "img-fluid rounded border";
col.appendChild(img);
imagesEl.appendChild(col);
}
} finally {
setLoading(false);
}
}
document.querySelectorAll(".folder-card").forEach(card => {
card.addEventListener("click", () => {
const title = card.dataset.title || "Files";
const date = card.dataset.date || "";
const author = card.dataset.author || "";
const count = card.dataset.count || "0";
const keywords = card.dataset.keywords || "";
titleEl.textContent = title;
metaEl.textContent = `${date} · ${author} · ${count} files`;
renderKeywords(keywords);
const folderId = card.dataset.folderId;
if (folderId) {
loadImages(folderId);
folderModal.show();
}
});
});
</script>
}