345 lines
14 KiB
HTML
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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
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 %}
|