h-lab/src/main/resources/templates/channels.html

271 lines
14 KiB
HTML

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/base}">
<head>
<title>h-lab - Channels</title>
</head>
<body>
<div layout:fragment="content">
<header class="flex justify-between items-center mb-4" style="margin-bottom: 2rem;">
<div>
<h1 class="text-xl font-bold mb-4">Channels</h1>
<p class="text-muted">Manage the YouTube channels you are tracking.</p>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer text-sm text-muted hover:text-white transition-colors">
<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
Select All
</label>
<button id="viewSelectedBtn" class="btn btn-outline" onclick="viewSelectedVideos()" style="display: none;">
<i data-lucide="play" style="width: 18px; margin-right: 8px;"></i> View Selected Videos (<span id="selectedCount">0</span>)
</button>
<button class="btn btn-primary" onclick="openAddModal()">
<i data-lucide="plus" style="width: 18px; margin-right: 8px;"></i> Add Channel
</button>
</div>
</header>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem;">
<!-- Channel Card Loop -->
<div class="card relative cursor-pointer hover:border-[var(--primary)] transition-all"
th:each="channel : ${channels}"
th:onclick="'toggleCardSelection(this, event, ' + ${channel.id} + ')'"
style="user-select: none;">
<!-- Custom Checkbox -->
<div class="absolute top-4 left-4 z-10 flex items-center justify-center" style="width: 28px; height: 28px;">
<input type="checkbox" class="channel-checkbox cursor-pointer opacity-0 absolute w-full h-full z-20"
th:value="${channel.id}"
onchange="updateSelectedCount(); event.stopPropagation();">
<div class="custom-checkbox flex items-center justify-center" style="
width: 24px; height: 24px;
border: 2px solid rgba(255,255,255,0.2);
border-radius: 6px;
background: rgba(255,255,255,0.05);
transition: all 0.2s ease;
pointer-events: none;
">
<i data-lucide="check" style="width: 16px; height: 16px; color: white; display: none;"></i>
</div>
</div>
<div class="flex justify-between items-start mb-4" style="padding-left: 2.5rem;">
<div class="flex items-center gap-2">
<img th:src="${channel.thumbnailUrl}" alt="Thumbnail"
style="width: 48px; height: 48px; border-radius: 50%; object-fit: cover; background: var(--bg-primary);">
<div>
<h3 class="font-bold" th:text="${channel.title}">Tech Reviewer A</h3>
<div class="text-muted text-sm"
th:text="${#numbers.formatInteger(channel.subscriberCount ?: 0, 0, 'COMMA')} + ' Subscribers'">
1.2M Subscribers</div>
</div>
</div>
</div>
<div class="mb-4 text-sm text-muted" style="
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 2.5em;
" th:text="${channel.description}">
Channel description goes here...
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
<div class="p-4"
style="background: rgba(255,255,255,0.03); border-radius: 8px; text-align: center;">
<div class="text-muted mb-4" style="font-size: 0.75rem">Videos</div>
<div class="font-bold" th:text="${#numbers.formatInteger(channel.videoCount ?: 0, 0, 'COMMA')}">450
</div>
</div>
<div class="p-4"
style="background: rgba(255,255,255,0.03); border-radius: 8px; text-align: center;">
<div class="text-muted mb-4" style="font-size: 0.75rem">Total Views</div>
<div class="font-bold" th:text="${#numbers.formatInteger(channel.viewCount ?: 0, 0, 'COMMA')}">45K
</div>
</div>
</div>
<div class="flex gap-2" onclick="event.stopPropagation()">
<a th:href="@{/channels/{id}(id=${channel.id})}" class="btn btn-primary w-full"
style="font-size: 0.8rem; text-decoration: none; display: flex; align-items: center; justify-content: center;">
<i data-lucide="list" style="width: 14px; margin-right: 6px;"></i> Detail
</a>
<a th:href="'https://www.youtube.com/channel/' + ${channel.channelId}" target="_blank"
class="btn btn-ghost w-full"
style="font-size: 0.8rem; text-decoration: none; display: flex; align-items: center; justify-content: center;">
<i data-lucide="external-link" style="width: 14px; margin-right: 6px;"></i> Visit
</a>
<button class="btn btn-ghost w-full" style="color: var(--danger); font-size: 0.8rem;" th:onclick="'deleteChannel(' + ${channel.id} + '); event.stopPropagation();'">
<i data-lucide="trash-2" style="width: 14px; margin-right: 6px;"></i> Remove
</button>
</div>
</div>
</div>
<!-- Add Channel Modal -->
<div id="addChannelModal"
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div
style="background: var(--bg-secondary); padding: 2rem; border-radius: 12px; width: 100%; max-width: 500px; border: 1px solid var(--border-color);">
<h2 class="text-xl font-bold mb-4">Add YouTube Channel</h2>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Channel URL</label>
<input type="text" id="channelUrlInput" placeholder="https://www.youtube.com/@ChannelHandle"
style="width: 100%; padding: 0.75rem; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-primary);">
</div>
<div class="flex justify-end gap-2">
<button class="btn btn-ghost" onclick="closeAddModal()">Cancel</button>
<button class="btn btn-primary" onclick="submitAddChannel()">Add Channel</button>
</div>
</div>
</div>
<script>
function openAddModal() {
const modal = document.getElementById('addChannelModal');
modal.style.display = 'flex';
document.getElementById('channelUrlInput').focus();
}
function closeAddModal() {
document.getElementById('addChannelModal').style.display = 'none';
document.getElementById('channelUrlInput').value = '';
}
async function submitAddChannel() {
const url = document.getElementById('channelUrlInput').value;
if (!url) {
alert('Please enter a URL');
return;
}
try {
const response = await fetch('/api/channels', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: url })
});
if (response.ok) {
alert('Channel added successfully!');
closeAddModal();
window.location.reload();
} else {
const errorData = await response.json();
alert('Failed to add channel: ' + (errorData.message || 'Unknown error'));
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred while adding the channel.');
}
}
async function deleteChannel(id) {
if (!confirm('정말로 이 채널과 관련된 모든 데이터를 삭제하시겠습니까?\\n삭제된 데이터는 복구할 수 없습니다.')) {
return;
}
try {
const response = await fetch('/api/channels/' + id, {
method: 'DELETE'
});
if (response.ok) {
alert('채널이 성공적으로 삭제되었습니다.');
window.location.reload();
} else {
const err = await response.json().catch(() => ({}));
alert('채널 삭제에 실패했습니다: ' + (err.message || '서버 오류'));
}
} catch (error) {
console.error('Error:', error);
alert('서버 통신 중 오류가 발생했습니다.');
}
}
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.channel-checkbox');
const checked = document.querySelectorAll('.channel-checkbox:checked');
const count = checked.length;
document.getElementById('selectedCount').innerText = count;
document.getElementById('viewSelectedBtn').style.display = count > 0 ? 'flex' : 'none';
// Update select all checkbox state
const selectAll = document.getElementById('selectAllCheckbox');
if (count === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (count === checkboxes.length) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
selectAll.checked = false;
selectAll.indeterminate = true;
}
// Update card visual state
checkboxes.forEach(cb => {
const card = cb.closest('.card');
const customCb = card.querySelector('.custom-checkbox');
const checkIcon = customCb.querySelector('i');
if (cb.checked) {
card.style.borderColor = 'var(--primary)';
card.style.background = 'rgba(59, 130, 246, 0.05)';
customCb.style.background = 'var(--primary)';
customCb.style.borderColor = 'var(--primary)';
checkIcon.style.display = 'block';
} else {
card.style.borderColor = 'var(--glass-border)';
card.style.background = 'var(--glass-bg)';
customCb.style.background = 'rgba(255,255,255,0.05)';
customCb.style.borderColor = 'rgba(255,255,255,0.2)';
checkIcon.style.display = 'none';
}
});
if (window.lucide) window.lucide.createIcons();
}
function toggleSelectAll() {
const isChecked = document.getElementById('selectAllCheckbox').checked;
const checkboxes = document.querySelectorAll('.channel-checkbox');
checkboxes.forEach(cb => cb.checked = isChecked);
updateSelectedCount();
}
function toggleCardSelection(card, event, id) {
// If the user clicked on a link or button, don't toggle selection
if (event.target.closest('a') || event.target.closest('button') || event.target.closest('input')) {
return;
}
const cb = card.querySelector('.channel-checkbox');
cb.checked = !cb.checked;
updateSelectedCount();
}
function viewSelectedVideos() {
const checkboxes = document.querySelectorAll('.channel-checkbox:checked');
const ids = Array.from(checkboxes).map(cb => cb.value).join(',');
if (ids) {
window.location.href = '/channels/videos?ids=' + ids;
}
}
// Close modal on outside click
window.onclick = function (event) {
const modal = document.getElementById('addChannelModal');
if (event.target == modal) {
closeAddModal();
}
}
</script>
</div>
</body>
</html>