Files
urllists/static/templates/dashboard.html
2025-11-30 12:53:36 +00:00

345 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard - {{ app_name }}{% endblock %}
{% block content %}
<div class="space-y-4">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 class="text-3xl font-bold">URL Lists</h1>
</div>
<a href="{{ url_for('create_list') }}" class="bg-sky-600 hover:bg-sky-700 text-white font-semibold py-2 px-4 rounded transition duration-200 text-center text-sm whitespace-nowrap">
+ New List
</a>
</div>
<!-- Search and Filter Section -->
<div class="space-y-2">
<!-- My Lists Search -->
<div class="relative">
<input
type="text"
id="searchInput"
placeholder="Search your lists..."
class="w-full bg-slate-900 border border-slate-700 rounded px-4 py-2 text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500 transition text-sm"
>
</div>
<!-- Search Public Lists -->
<div class="relative">
<input
type="text"
id="publicSearchInput"
placeholder="Search public lists by domain (e.g., github.com)..."
class="w-full bg-slate-900 border border-slate-700 rounded px-4 py-2 text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500 transition text-sm"
>
</div>
</div>
<!-- My Lists Table -->
<div id="myListsSection" class="space-y-2">
<h2 class="text-lg font-semibold text-slate-300">My Lists</h2>
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-700 bg-slate-900">
<th class="px-6 py-3 text-left font-semibold text-slate-300">Name</th>
<th class="px-6 py-3 text-left font-semibold text-slate-300">Slug</th>
<th class="px-6 py-3 text-left font-semibold text-slate-300">URLs</th>
<th class="px-6 py-3 text-left font-semibold text-slate-300">Visibility</th>
<th class="px-6 py-3 text-right font-semibold text-slate-300">Actions</th>
</tr>
</thead>
<tbody id="listContainer" class="divide-y divide-slate-700">
<!-- Loading State -->
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-400">Loading lists...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Empty State (hidden by default) -->
<div id="emptyState" class="hidden bg-slate-800 rounded-lg border border-slate-700 p-12 text-center">
<div class="text-4xl mb-3">📝</div>
<p class="text-slate-400 mb-4 text-sm">No lists yet. Create one to get started!</p>
<a href="{{ url_for('create_list') }}" class="inline-block bg-sky-600 hover:bg-sky-700 text-white font-semibold py-2 px-6 rounded transition duration-200 text-sm">
Create Your First List
</a>
</div>
<!-- Public Lists Section -->
<div id="publicListsSection" class="space-y-2 mt-8 hidden">
<h2 class="text-lg font-semibold text-slate-300">Public Lists (Search Results)</h2>
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-700 bg-slate-900">
<th class="px-6 py-3 text-left font-semibold text-slate-300">Name</th>
<th class="px-6 py-3 text-left font-semibold text-slate-300">URLs</th>
<th class="px-6 py-3 text-left font-semibold text-slate-300">Updated</th>
<th class="px-6 py-3 text-right font-semibold text-slate-300">Action</th>
</tr>
</thead>
<tbody id="publicListContainer" class="divide-y divide-slate-700">
<tr>
<td colspan="4" class="px-6 py-8 text-center text-slate-400">Search public lists by entering a domain...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!isAuthenticated()) {
window.location.href = "{{ url_for('login') }}";
return;
}
loadLists();
});
let allLists = [];
function loadLists() {
const token = localStorage.getItem('access_token');
fetch('/api/url-lists/', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (response.status === 401) {
localStorage.removeItem('access_token');
window.location.href = "{{ url_for('login') }}";
return;
}
return response.json();
})
.then(lists => {
allLists = lists || [];
renderTable();
})
.catch(error => {
console.error('Error loading lists:', error);
document.getElementById('listContainer').innerHTML = `<tr><td colspan="5" class="px-6 py-8 text-center text-red-400">Failed to load lists</td></tr>`;
});
}
function renderTable() {
const container = document.getElementById('listContainer');
const myListsSection = document.getElementById('myListsSection');
const emptyState = document.getElementById('emptyState');
if (!allLists || allLists.length === 0) {
myListsSection.classList.add('hidden');
emptyState.classList.remove('hidden');
return;
}
myListsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
// Create table rows
let html = '';
allLists.forEach(list => {
const urlCount = list.urls.split('\n').filter(u => u.trim()).length;
const visibilityBadge = list.is_public ? '<span class="inline-block bg-green-900 text-green-300 text-xs px-2 py-1 rounded">🌐 Public</span>' : '<span class="inline-block bg-slate-700 text-slate-300 text-xs px-2 py-1 rounded">🔒 Private</span>';
html += `
<tr class="hover:bg-slate-700 transition">
<td class="px-6 py-4 font-semibold text-slate-100">${escapeHtml(list.name)}</td>
<td class="px-6 py-4">
<span class="text-xs font-mono bg-slate-900 px-2 py-1 rounded inline-block text-sky-400">${escapeHtml(list.unique_slug)}</span>
</td>
<td class="px-6 py-4 text-slate-300">${urlCount} URL${urlCount !== 1 ? 's' : ''}</td>
<td class="px-6 py-4">${visibilityBadge}</td>
<td class="px-6 py-4 text-right">
<div class="flex gap-2 justify-end flex-wrap">
<a href="{{ url_for('list_read', slug='') }}${encodeURIComponent(list.unique_slug)}" target="_blank" class="inline-flex items-center justify-center w-8 h-8 bg-slate-700 hover:bg-sky-600 rounded transition duration-200 text-sm" title="View">
👁️
</a>
<button onclick="editList('${list.id}')" class="inline-flex items-center justify-center w-8 h-8 bg-slate-700 hover:bg-slate-600 rounded transition duration-200 text-sm" title="Edit">
✏️
</button>
<button onclick="togglePublic('${list.id}', ${list.is_public})" class="inline-flex items-center justify-center w-8 h-8 ${list.is_public ? 'bg-green-900 hover:bg-green-800' : 'bg-slate-700 hover:bg-slate-600'} rounded transition duration-200 text-sm" title="${list.is_public ? 'Make Private' : 'Make Public'}">
${list.is_public ? '🔓' : '🔒'}
</button>
<button onclick="deleteList('${list.id}')" class="inline-flex items-center justify-center w-8 h-8 bg-slate-700 hover:bg-red-600 rounded transition duration-200 text-sm" title="Delete">
🗑️
</button>
</div>
</td>
</tr>
`;
});
container.innerHTML = html;
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// Search my lists functionality
document.getElementById('searchInput').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const container = document.getElementById('listContainer');
if (!query) {
renderTable();
return;
}
const filtered = allLists.filter(list =>
list.name.toLowerCase().includes(query) ||
list.unique_slug.toLowerCase().includes(query)
);
if (filtered.length === 0) {
container.innerHTML = `
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-400">No lists found matching "${escapeHtml(query)}"</td>
</tr>
`;
return;
}
// Temporarily set allLists for rendering
const temp = allLists;
allLists = filtered;
renderTable();
allLists = temp;
});
// Search public lists functionality with debouncing
let publicSearchTimeout;
document.getElementById('publicSearchInput').addEventListener('input', (e) => {
clearTimeout(publicSearchTimeout);
const query = e.target.value.trim();
const publicSection = document.getElementById('publicListsSection');
const container = document.getElementById('publicListContainer');
if (!query) {
publicSection.classList.add('hidden');
container.innerHTML = `<tr><td colspan="4" class="px-6 py-8 text-center text-slate-400">Search public lists by entering a domain...</td></tr>`;
return;
}
publicSection.classList.remove('hidden');
container.innerHTML = `<tr><td colspan="4" class="px-6 py-8 text-center text-slate-400">Searching...</td></tr>`;
// Wait 500ms after user stops typing before searching
publicSearchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/public-lists/search?domain=${encodeURIComponent(query)}`);
const lists = await response.json();
if (lists.length === 0) {
container.innerHTML = `<tr><td colspan="4" class="px-6 py-8 text-center text-slate-400">No public lists found containing "${escapeHtml(query)}"</td></tr>`;
return;
}
let html = '';
lists.forEach(list => {
const urlCount = list.urls.split('\n').filter(u => u.trim()).length;
const updatedDate = new Date(list.updated_at).toLocaleDateString();
html += `
<tr class="hover:bg-slate-700 transition">
<td class="px-6 py-4 font-semibold text-slate-100">${escapeHtml(list.name)}</td>
<td class="px-6 py-4 text-slate-300">${urlCount} URL${urlCount !== 1 ? 's' : ''}</td>
<td class="px-6 py-4 text-slate-400 text-xs">${updatedDate}</td>
<td class="px-6 py-4 text-right">
<a href="{{ url_for('list_read', slug='') }}${encodeURIComponent(list.unique_slug)}" target="_blank" class="inline-flex items-center justify-center w-8 h-8 bg-slate-700 hover:bg-sky-600 rounded transition duration-200 text-sm" title="View">
👁️
</a>
</td>
</tr>
`;
});
container.innerHTML = html;
} catch (error) {
console.error('Error searching public lists:', error);
container.innerHTML = `<tr><td colspan="4" class="px-6 py-8 text-center text-red-400">Error searching public lists</td></tr>`;
}
}, 500);
});
function editList(listId) {
const token = localStorage.getItem('access_token');
const list = allLists.find(l => l.id === listId);
if (!list) return;
// Store list data and redirect to create page for editing
sessionStorage.setItem('editList', JSON.stringify({
id: list.id,
name: list.name,
unique_slug: list.unique_slug,
urls: list.urls,
is_public: list.is_public
}));
window.location.href = '{{ url_for("create_list") }}';
}
async function togglePublic(listId, currentPublic) {
const token = localStorage.getItem('access_token');
const newPublic = !currentPublic;
try {
const response = await fetch(`/api/url-lists/${listId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ is_public: newPublic })
});
if (response.ok) {
loadLists();
} else {
alert('Failed to update list visibility');
}
} catch (error) {
console.error('Error updating list:', error);
alert('Error updating list visibility');
}
}
function deleteList(listId) {
if (!confirm('Are you sure you want to delete this list? This action cannot be undone.')) return;
const token = localStorage.getItem('access_token');
fetch(`/api/url-lists/${listId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (response.ok) {
loadLists();
} else {
alert('Failed to delete list');
}
});
}
</script>
{% endblock %}