251 lines
11 KiB
HTML
251 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html data-bs-theme="dark" lang="en">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||
<title>Photo Viewer | Premium Gallery</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-icons@1.13.1/font/bootstrap-icons.min.css">
|
||
|
||
<style>
|
||
body {
|
||
background-color: #0b0c10;
|
||
color: #ffffff;
|
||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||
}
|
||
|
||
/* Glassmorphism Navbar */
|
||
.navbar {
|
||
background: rgba(11, 12, 16, 0.8) !important;
|
||
backdrop-filter: blur(10px);
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||
}
|
||
|
||
/* Photo Display Stage */
|
||
.photo-stage {
|
||
background: radial-gradient(circle, #1f2833 0%, #0b0c10 100%);
|
||
border-radius: 1.5rem;
|
||
padding: 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 500px;
|
||
box-shadow: inset 0 0 50px rgba(0,0,0,0.5);
|
||
border: 1px solid rgba(255,255,255,0.05);
|
||
}
|
||
|
||
.photo-stage img {
|
||
max-height: 75vh;
|
||
object-fit: contain;
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
||
}
|
||
|
||
/* Refined Cards */
|
||
.card {
|
||
background: rgba(31, 40, 51, 0.4);
|
||
backdrop-filter: blur(5px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 1rem;
|
||
}
|
||
|
||
.form-control, .form-control:focus {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
color: white;
|
||
border-radius: 0.5rem;
|
||
}
|
||
|
||
.form-label {
|
||
color: rgba(255,255,255,0.6);
|
||
font-size: 0.85rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
/* Action Buttons */
|
||
.btn-action-group .btn {
|
||
background: rgba(255,255,255,0.05);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
color: white;
|
||
padding: 0.5rem 1rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-action-group .btn:hover {
|
||
background: rgba(255,255,255,0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* Metadata List */
|
||
.meta-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0.75rem 0;
|
||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||
}
|
||
|
||
.meta-label { color: rgba(255,255,255,0.5); font-size: 0.9rem; }
|
||
.meta-value { font-weight: 500; font-size: 0.9rem; }
|
||
|
||
@media (max-width: 991px) {
|
||
.photo-stage { min-height: 350px; padding: 1rem; }
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body hx-boost="true">
|
||
|
||
<nav class="navbar navbar-expand-md sticky-top">
|
||
<div class="container">
|
||
<a class="navbar-brand fw-bold text-uppercase tracking-wider" href="/home">
|
||
<i class="bi bi-camera-reels me-2 text-primary"></i>Gallery
|
||
</a>
|
||
<div class="ms-auto">
|
||
<a class="btn btn-sm btn-outline-light rounded-pill px-4" href="/home">
|
||
<i class="bi bi-arrow-left me-2"></i>Back to Grid
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container py-4 py-md-5">
|
||
<div class="row g-4">
|
||
|
||
<div class="col-lg-8">
|
||
<div class="photo-stage mb-4">
|
||
<img th:src="@{${photo.filePath}}" alt="Main photo" class="img-fluid">
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-center">
|
||
<div class="btn-group btn-action-group rounded-pill overflow-hidden shadow">
|
||
<a th:href="@{${photo.filePath}}" class="btn" download title="Download">
|
||
<i class="bi bi-download"></i>
|
||
</a>
|
||
<a th:href="@{${photo.filePath}}" class="btn" target="_blank" title="Open original">
|
||
<i class="bi bi-box-arrow-up-right"></i>
|
||
</a>
|
||
<button class="btn" th:fragment="favButton"
|
||
th:attr="hx-post=@{/photo/favorite/{id}(id=${photo.id})}"
|
||
hx-swap="outerHTML" hx-boost="false">
|
||
<i th:if="${!photo.favourite}" class="bi bi-star"></i>
|
||
<i th:if="${photo.favourite}" class="bi bi-star-fill text-warning"></i>
|
||
</button>
|
||
<button class="btn" title="Fullscreen"
|
||
onclick="document.querySelector('.photo-stage img').requestFullscreen()">
|
||
<i class="bi bi-arrows-fullscreen"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<form th:object="${photo}" method="post" th:action="@{/photo/{id}(id=${photo.id})}" class="card mb-4 shadow-sm" hx-boost="false">
|
||
<div class="card-body p-4">
|
||
<h5 class="fw-bold mb-4"><i class="bi bi-pencil-square me-2 text-primary"></i>Properties</h5>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Title</label>
|
||
<input th:field="*{title}" class="form-control" type="text" placeholder="Add a title...">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Description</label>
|
||
<textarea th:field="*{description}" class="form-control" rows="3" placeholder="Describe this moment..."></textarea>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Tags</label>
|
||
<div class="input-group">
|
||
<span class="input-group-text bg-transparent border-end-0 text-white-50"><i class="bi bi-tag"></i></span>
|
||
<input name="newTag" class="form-control border-start-0" maxlength="20"
|
||
placeholder="Add tag & hit Enter"
|
||
th:attr="hx-post=@{/tags/add(photoId=${photo.id})}"
|
||
hx-trigger="keyup[key=='Enter']"
|
||
hx-target="#tagContainer"
|
||
hx-swap="beforeend"
|
||
hx-on::after-request="this.value=''"
|
||
hx-boost="false" />
|
||
</div>
|
||
<div id="tagContainer" class="mt-3 d-flex flex-wrap gap-2">
|
||
<th:block th:each="tag : *{tags}">
|
||
<div th:replace="~{:: tagBadge(tag=${tag})}"></div>
|
||
</th:block>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<label class="form-label">Copyright</label>
|
||
<input th:field="*{copyRight}" class="form-control" type="text">
|
||
</div>
|
||
|
||
<div class="row g-2">
|
||
<div class="col-6">
|
||
<button class="btn btn-outline-light w-100 rounded-pill" type="reset">Reset</button>
|
||
</div>
|
||
<div class="col-6">
|
||
<button class="btn btn-primary w-100 rounded-pill" type="submit">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<span th:fragment="tagBadge(tag)" class="badge rounded-pill bg-primary bg-opacity-25 border border-primary border-opacity-50 px-3 py-2 d-flex align-items-center">
|
||
<span th:text="${tag}"></span>
|
||
<input type="hidden" name="tags" th:value="${tag}">
|
||
<button type="button" class="btn-close btn-close-white ms-2" style="font-size: 0.6rem;"
|
||
hx-post="/tags/remove"
|
||
hx-target="closest .badge"
|
||
hx-swap="outerHTML"
|
||
hx-boost="false">
|
||
</button>
|
||
</span>
|
||
|
||
<div class="card mb-4 shadow-sm">
|
||
<div class="card-body p-4">
|
||
<h6 class="fw-bold mb-3"><i class="bi bi-info-circle me-2 text-primary"></i>Technical Info</h6>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Format</span>
|
||
<span class="meta-value text-uppercase" th:text="${photo.mimeType}"></span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Resolution</span>
|
||
<span class="meta-value" th:text="${photo.width} + ' × ' + ${photo.height}"></span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Size</span>
|
||
<span class="meta-value" th:text="${photo.file_size} + ' MB'"></span>
|
||
</div>
|
||
<div class="meta-row border-0">
|
||
<span class="meta-label">Color Space</span>
|
||
<span class="meta-value" th:text="${photo.colourSpace}"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card overflow-hidden shadow-sm">
|
||
<div class="card-body p-0">
|
||
<iframe src="https://cdn.bootstrapstudio.io/placeholders/map.html" width="100%" height="200" style="border:0; filter: invert(90%) hue-rotate(180deg);" loading="lazy"></iframe>
|
||
</div>
|
||
</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://unpkg.com/htmx.org@1.9.12"></script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
document.body.addEventListener("keydown", function(e) {
|
||
if (e.target.name === "newTag" && e.key === "Enter") {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html> |