You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
11 KiB
245 lines
11 KiB
{% extends "layout.html" %}
|
|
{% block main %}
|
|
|
|
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] py-8">
|
|
<div class="container mx-auto px-4">
|
|
<!-- Header & Actions -->
|
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-8">
|
|
<h1 class="text-3xl font-display font-black uppercase tracking-tighter text-[#111111]">Background Manager</h1>
|
|
<div class="flex w-full md:w-auto gap-2">
|
|
<div class="relative flex-grow md:w-64">
|
|
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#111111]/40"></i>
|
|
<input type="text"
|
|
class="searchFilter input-neo w-full pl-10"
|
|
placeholder="Search..."
|
|
onkeyup="searchFilter()">
|
|
</div>
|
|
<button onclick="add_modal.showModal()" class="btn-primary-neo text-sm">
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
<span class="hidden sm:inline ml-1">Add Video</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Background Grid -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" id="backgrounds">
|
|
<!-- Backgrounds will be injected here -->
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="empty-state" class="hidden flex flex-col items-center justify-center py-20 text-[#111111]/30">
|
|
<i data-lucide="film" class="w-16 h-16 mb-4"></i>
|
|
<p class="text-lg font-mono uppercase tracking-wider">No backgrounds found</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Background Modal -->
|
|
<dialog id="delete_modal" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box bg-white border border-[#111111]">
|
|
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Delete Background</h3>
|
|
<p class="py-4 text-[#111111]/60 font-mono text-sm">Are you sure you want to delete this background video? This action cannot be undone.</p>
|
|
<div class="modal-action">
|
|
<form action="background/delete" method="post" class="flex gap-2">
|
|
<input type="hidden" id="background-key" name="background-key" value="">
|
|
<button type="button" onclick="delete_modal.close()" class="btn-ghost-neo">Cancel</button>
|
|
<button type="submit" class="btn-danger-neo text-sm">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- Add Background Modal -->
|
|
<dialog id="add_modal" class="modal modal-bottom sm:modal-middle">
|
|
<div class="modal-box bg-white border border-[#111111] max-w-lg">
|
|
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111] mb-6">Add Background Video</h3>
|
|
<form id="addBgForm" action="background/add" method="post" novalidate class="space-y-4">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">YouTube URI</span></label>
|
|
<div class="flex border border-[#111111]">
|
|
<div class="flex items-center justify-center px-3 bg-[#111111]/5 border-r border-[#111111] shrink-0">
|
|
<i data-lucide="youtube" class="w-4 h-4 text-[#111111]"></i>
|
|
</div>
|
|
<input name="youtube_uri" type="text" placeholder="https://www.youtube.com/watch?v=..."
|
|
class="input-neo border-0 flex-1">
|
|
</div>
|
|
<label class="label h-6"><span id="feedbackYT" class="label-text-alt text-[#DE6C56] font-mono text-xs hidden"></span></label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">Filename</span></label>
|
|
<div class="flex border border-[#111111]">
|
|
<div class="flex items-center justify-center px-3 bg-[#111111]/5 border-r border-[#111111] shrink-0">
|
|
<i data-lucide="file-video" class="w-4 h-4 text-[#111111]"></i>
|
|
</div>
|
|
<input name="filename" type="text" placeholder="e.g. minecraft-parkour"
|
|
class="input-neo border-0 flex-1">
|
|
</div>
|
|
<label class="label h-6"><span id="feedbackFilename" class="label-text-alt text-[#DE6C56] font-mono text-xs hidden"></span></label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">Credits</span></label>
|
|
<div class="flex border border-[#111111]">
|
|
<div class="flex items-center justify-center px-3 bg-[#111111]/5 border-r border-[#111111] shrink-0">
|
|
<i data-lucide="user" class="w-4 h-4 text-[#111111]"></i>
|
|
</div>
|
|
<input name="citation" type="text" placeholder="YouTube Channel Name"
|
|
class="input-neo border-0 flex-1">
|
|
</div>
|
|
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Name of the video owner.</span></label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/70 font-mono text-xs uppercase tracking-wider">Advanced: Position</span></label>
|
|
<input name="position" type="text" placeholder="center (optional)"
|
|
class="input-neo w-full text-sm h-10">
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" onclick="add_modal.close()" class="btn-ghost-neo">Cancel</button>
|
|
<button type="submit" class="btn-primary-neo text-sm">Add Background</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</dialog>
|
|
|
|
<script>
|
|
let keys = [];
|
|
let youtube_urls = [];
|
|
|
|
function h(str) {
|
|
return String(str ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
async function loadBackgrounds() {
|
|
try {
|
|
const response = await fetch("backgrounds.json");
|
|
const data = await response.json();
|
|
delete data["__comment"];
|
|
|
|
const container = document.getElementById('backgrounds');
|
|
let html = '';
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
keys.push(key);
|
|
youtube_urls.push(value[0]);
|
|
|
|
const videoId = value[0].includes('?v=') ? value[0].split('?v=')[1] : value[0].split('/').pop();
|
|
|
|
html += `
|
|
<div class="bg-card group bg-white border border-[#111111] hover:border-[#DE6C56] transition-colors duration-200">
|
|
<div class="aspect-video w-full bg-black border-b border-[#111111] relative">
|
|
<iframe class="w-full h-full"
|
|
src="https://www.youtube-nocookie.com/embed/${videoId}"
|
|
title="YouTube video player"
|
|
frameborder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowfullscreen></iframe>
|
|
</div>
|
|
<div class="p-4">
|
|
<h3 class="text-[#111111] font-mono text-sm font-medium truncate mb-1" title="${h(key)}">${h(key)}</h3>
|
|
<p class="text-[#111111]/40 font-mono text-xs truncate mb-4">${h(value[2])}</p>
|
|
|
|
<div class="flex justify-end">
|
|
<button onclick="confirmDelete('${key}')" class="btn-ghost-neo p-2 hover:text-[#DE6C56]">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
lucide.createIcons();
|
|
} catch (error) {
|
|
console.error("Error loading backgrounds:", error);
|
|
}
|
|
}
|
|
|
|
function confirmDelete(key) {
|
|
document.getElementById('background-key').value = key;
|
|
delete_modal.showModal();
|
|
}
|
|
|
|
function searchFilter() {
|
|
const query = document.querySelector(".searchFilter") ? document.querySelector(".searchFilter").value.toLowerCase() : '';
|
|
const cards = document.querySelectorAll(".bg-card");
|
|
let visibleCount = 0;
|
|
|
|
cards.forEach(card => {
|
|
const text = card.textContent.toLowerCase();
|
|
const matches = text.includes(query);
|
|
card.classList.toggle('hidden', !matches);
|
|
if (matches) visibleCount++;
|
|
});
|
|
|
|
document.getElementById('empty-state').classList.toggle('hidden', visibleCount > 0);
|
|
}
|
|
|
|
const form = document.getElementById('addBgForm');
|
|
form.addEventListener('submit', (e) => {
|
|
let isValid = true;
|
|
form.querySelectorAll('input').forEach(input => {
|
|
if (!validate(input)) isValid = false;
|
|
});
|
|
if (!isValid) e.preventDefault();
|
|
});
|
|
|
|
form.querySelectorAll('input').forEach(input => {
|
|
input.addEventListener('keyup', () => validate(input));
|
|
});
|
|
|
|
function validate(input) {
|
|
const name = input.name;
|
|
const value = input.value;
|
|
let valid = true;
|
|
let message = "";
|
|
|
|
if (name === "youtube_uri") {
|
|
const regex = /(?:\/|%3D|v=|vi=)([0-9A-Za-z_-]{11})(?:[%#?&]|$)/;
|
|
if (!regex.test(value)) {
|
|
message = "Invalid YouTube URI";
|
|
valid = false;
|
|
} else if (youtube_urls.includes(value)) {
|
|
message = "Background already added";
|
|
valid = false;
|
|
}
|
|
const feedback = document.getElementById('feedbackYT');
|
|
feedback.textContent = message;
|
|
feedback.classList.toggle('hidden', valid);
|
|
}
|
|
|
|
if (name === "filename") {
|
|
if (keys.includes(value)) {
|
|
message = "Filename already taken";
|
|
valid = false;
|
|
} else if (!/^([a-zA-Z0-9\s_-]{1,100})$/.test(value)) {
|
|
valid = false;
|
|
}
|
|
const feedback = document.getElementById('feedbackFilename');
|
|
feedback.textContent = message;
|
|
feedback.classList.toggle('hidden', valid);
|
|
}
|
|
|
|
if (name === "position") {
|
|
if (value && value !== "center" && isNaN(parseFloat(value))) {
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
input.classList.toggle('input-error', !valid);
|
|
return valid;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadBackgrounds);
|
|
</script>
|
|
|
|
{% endblock %}
|