Files
atakanozbancom/Views/myprojects/projectposts.cshtml
T
Atakan Doğan Özban e97a71e5b2 Add password hashing, user settings with 2FA, and project image lightbox.
Hardens auth with PBKDF2, lockouts, local QR setup, and safer embeds while moving account security under the username menu.
2026-07-17 18:49:34 +02:00

452 lines
16 KiB
Plaintext

@model IEnumerable<atakanozbancom.Models.classes.MyProjectsVM>
<p class="text-center">
@(atakanozbancom.App_GlobalResources.Resource.MyProjectsP1)
<a href="https://git.atakanozban.com/gitea" style="text-decoration:none;color:darkgrey;" target="_blank">
@atakanozbancom.App_GlobalResources.Resource.Gitea
</a>
@atakanozbancom.App_GlobalResources.Resource.MyProjectsP1_1
</p>
<style>
.carousel-media-iframe {
width: 100%;
aspect-ratio: 16 / 9;
position: relative;
overflow: hidden;
background: #000;
}
.carousel-media-iframe iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
.carousel-media-image {
width: 100%;
position: relative;
background: transparent;
overflow: hidden;
}
.carousel-media-image img {
display: block;
width: 100%;
height: auto;
object-fit: contain;
cursor: zoom-in;
}
.media-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 4rem;
height: 4rem;
opacity: 0.9;
bottom: auto;
z-index: 5;
}
.carousel-control-prev {
left: 1rem;
}
.carousel-control-next {
right: 1rem;
}
.carousel-indicators {
z-index: 6;
}
#projectLightboxModal .modal-dialog {
width: auto;
max-width: calc(100vw - 2rem);
margin: 0.5rem auto;
}
#projectLightboxModal .modal-content {
background: transparent;
width: fit-content;
max-width: 100%;
margin: 0 auto;
}
#projectLightboxModal .modal-body {
padding: 0;
}
#projectLightboxModal .lightbox-stage {
position: relative;
display: inline-block;
max-width: calc(100vw - 2rem);
line-height: 0;
background: #000;
border-radius: 0.35rem;
overflow: hidden;
}
#projectLightboxModal .lightbox-stage img {
display: block;
width: auto;
height: auto;
max-width: calc(100vw - 2rem);
max-height: 85vh;
object-fit: contain;
}
#projectLightboxModal .lightbox-arrow {
position: absolute;
top: 50%;
bottom: auto;
transform: translateY(-50%);
width: 3.5rem;
height: 3.5rem;
opacity: 0.9;
z-index: 2;
}
#projectLightboxModal .lightbox-arrow.carousel-control-prev {
left: 0;
}
#projectLightboxModal .lightbox-arrow.carousel-control-next {
right: 0;
}
#projectLightboxModal .lightbox-indicators {
position: absolute;
left: 0;
right: 0;
bottom: 0.75rem;
margin: 0;
z-index: 2;
}
#projectLightboxModal .lightbox-indicators button {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(255, 255, 255, .5);
border: 0;
margin: 0 4px;
padding: 0;
}
#projectLightboxModal .lightbox-indicators button.active {
background-color: #fff;
}
#projectLightboxModal .lightbox-close {
position: absolute;
top: 0.6rem;
right: 0.6rem;
z-index: 3;
}
</style>
@foreach (var p in Model)
{
var media = p.medias ?? new List<atakanozbancom.Models.classes.ProjectMediaVM>();
var carouselId = $"projCarousel_{p.id}";
var hasMultiple = media.Count > 1;
<div class="container py-4">
@if (media.Count > 0)
{
<div id="@carouselId" class="carousel slide" data-bs-ride="carousel">
@if (hasMultiple)
{
<div class="carousel-indicators">
@for (int i = 0; i < media.Count; i++)
{
<button type="button"
data-bs-target="#@carouselId"
data-bs-slide-to="@i"
class="@(i == 0 ? "active" : "")"
aria-current="@(i == 0 ? "true" : null)"
aria-label="Slide @(i + 1)"></button>
}
</div>
}
<div class="carousel-inner">
@for (int i = 0; i < media.Count; i++)
{
var m = media[i];
<div class="carousel-item @(i == 0 ? "active" : "")"
data-bs-interval="false">
@if (m.type == "iframe")
{
<div class="carousel-media-iframe">
<iframe src="@m.url"
loading="lazy"
allowfullscreen
referrerpolicy="no-referrer"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
@if (hasMultiple)
{
<button class="carousel-control-prev media-arrow"
type="button"
data-bs-target="#@carouselId"
data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next media-arrow"
type="button"
data-bs-target="#@carouselId"
data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
}
</div>
}
else if (m.type == "image")
{
<div class="carousel-media-image">
<img src="@m.url"
alt="@p.title"
class="project-lightbox-trigger"
data-fullsrc="@m.url"
data-gallery="@carouselId" />
@if (hasMultiple)
{
<button class="carousel-control-prev media-arrow"
type="button"
data-bs-target="#@carouselId"
data-bs-slide="prev">
<span class="carousel-control-prev-icon"></span>
</button>
<button class="carousel-control-next media-arrow"
type="button"
data-bs-target="#@carouselId"
data-bs-slide="next">
<span class="carousel-control-next-icon"></span>
</button>
}
</div>
}
@*else if (m.type == "video")
{
<video class="d-block w-100" controls preload="metadata">
<source src="@m.url" type="video/mp4" />
</video>
}*@
@*else if (m.type == "document")
{
<div class="ratio ratio-4x3">
<iframe src="@m.url"></iframe>
</div>
<div class="text-center mt-2">
<a href="@m.url" target="_blank">Open document</a>
</div>
}*@
</div>
}
</div>
</div>
}
<h3 class="mt-3 text-center">@p.title</h3>
<p class="lead mb-5 text-center">@Html.Raw(atakanozbancom.Models.classes.DescriptionFormatter.ToSafeHtml(p.description))</p>
</div>
}
<div class="modal fade" id="projectLightboxModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-none">
<div class="modal-body">
<div class="lightbox-stage">
<button type="button" class="btn-close btn-close-white lightbox-close" data-bs-dismiss="modal" aria-label="Close"></button>
<img id="projectLightboxImg" src="" alt="" />
<button type="button" id="lightboxPrev" class="carousel-control-prev lightbox-arrow" aria-label="Previous">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
</button>
<button type="button" id="lightboxNext" class="carousel-control-next lightbox-arrow" aria-label="Next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
</button>
<div id="lightboxIndicators" class="carousel-indicators lightbox-indicators d-none"></div>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
var items = [];
var index = 0;
var modalEl = null;
var imgEl = null;
var stageEl = null;
var dialogEl = null;
var indicatorsEl = null;
var prevBtn = null;
var nextBtn = null;
function getEls() {
modalEl = document.getElementById('projectLightboxModal');
imgEl = document.getElementById('projectLightboxImg');
indicatorsEl = document.getElementById('lightboxIndicators');
prevBtn = document.getElementById('lightboxPrev');
nextBtn = document.getElementById('lightboxNext');
stageEl = modalEl ? modalEl.querySelector('.lightbox-stage') : null;
dialogEl = modalEl ? modalEl.querySelector('.modal-dialog') : null;
return modalEl && imgEl && indicatorsEl && prevBtn && nextBtn && stageEl && dialogEl;
}
function fitToImage() {
var maxW = Math.max(window.innerWidth - 32, 200);
var maxH = Math.max(window.innerHeight * 0.85, 200);
var nw = imgEl.naturalWidth || 0;
var nh = imgEl.naturalHeight || 0;
if (!nw || !nh) return;
var scale = Math.min(maxW / nw, maxH / nh, 1);
var w = Math.round(nw * scale);
var h = Math.round(nh * scale);
imgEl.style.width = w + 'px';
imgEl.style.height = h + 'px';
stageEl.style.width = w + 'px';
stageEl.style.height = h + 'px';
dialogEl.style.width = w + 'px';
dialogEl.style.maxWidth = w + 'px';
}
function showSlide(i) {
if (!items.length) return;
index = (i + items.length) % items.length;
var item = items[index];
imgEl.onload = fitToImage;
imgEl.src = item.src;
imgEl.alt = item.alt;
if (imgEl.complete && imgEl.naturalWidth) fitToImage();
var dots = indicatorsEl.querySelectorAll('button');
for (var d = 0; d < dots.length; d++) {
dots[d].classList.toggle('active', d === index);
if (d === index) dots[d].setAttribute('aria-current', 'true');
else dots[d].removeAttribute('aria-current');
}
}
function buildIndicators() {
indicatorsEl.innerHTML = '';
if (items.length < 2) {
indicatorsEl.classList.add('d-none');
prevBtn.classList.add('d-none');
nextBtn.classList.add('d-none');
return;
}
indicatorsEl.classList.remove('d-none');
prevBtn.classList.remove('d-none');
nextBtn.classList.remove('d-none');
for (var i = 0; i < items.length; i++) {
(function (slideIndex) {
var btn = document.createElement('button');
btn.type = 'button';
btn.setAttribute('aria-label', 'Slide ' + (slideIndex + 1));
if (slideIndex === index) {
btn.classList.add('active');
btn.setAttribute('aria-current', 'true');
}
btn.addEventListener('click', function () { showSlide(slideIndex); });
indicatorsEl.appendChild(btn);
})(i);
}
}
document.addEventListener('click', function (e) {
var trigger = e.target.closest('.project-lightbox-trigger');
if (!trigger) return;
if (typeof bootstrap === 'undefined') return;
if (!getEls()) return;
e.preventDefault();
var gallery = trigger.getAttribute('data-gallery');
var galleryImgs = gallery
? document.querySelectorAll('.project-lightbox-trigger[data-gallery="' + gallery + '"]')
: [trigger];
items = [];
index = 0;
for (var i = 0; i < galleryImgs.length; i++) {
var img = galleryImgs[i];
items.push({
src: img.getAttribute('data-fullsrc') || img.src,
alt: img.alt || ''
});
if (img === trigger) index = i;
}
buildIndicators();
showSlide(index);
bootstrap.Modal.getOrCreateInstance(modalEl).show();
});
document.addEventListener('click', function (e) {
if (e.target.closest('#lightboxPrev')) {
e.preventDefault();
showSlide(index - 1);
} else if (e.target.closest('#lightboxNext')) {
e.preventDefault();
showSlide(index + 1);
}
});
document.addEventListener('keydown', function (e) {
if (!modalEl || !modalEl.classList.contains('show')) return;
if (e.key === 'ArrowLeft') showSlide(index - 1);
if (e.key === 'ArrowRight') showSlide(index + 1);
});
window.addEventListener('resize', function () {
if (modalEl && modalEl.classList.contains('show')) fitToImage();
});
document.addEventListener('hidden.bs.modal', function (e) {
if (e.target.id !== 'projectLightboxModal') return;
if (!imgEl) getEls();
if (imgEl) {
imgEl.onload = null;
imgEl.removeAttribute('src');
imgEl.style.width = '';
imgEl.style.height = '';
}
if (stageEl) {
stageEl.style.width = '';
stageEl.style.height = '';
}
if (dialogEl) {
dialogEl.style.width = '';
dialogEl.style.maxWidth = '';
}
items = [];
index = 0;
});
})();
</script>