284 lines
11 KiB
HTML
284 lines
11 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">
|
||
<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">Created By</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">
|
||
<!-- Loading State -->
|
||
<tr>
|
||
<td colspan="5" 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 = `
|
||
<div class="p-8 text-center">
|
||
<p class="text-red-400 text-sm">Failed to load lists</p>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
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>';
|
||
const visibilityToggle = `<button onclick="togglePublic('${list.id}', ${list.is_public})" class="text-xs py-1 px-2 rounded ${list.is_public ? 'bg-green-900 hover:bg-green-800 text-green-300' : 'bg-slate-700 hover:bg-slate-600 text-slate-300'} transition">${list.is_public ? 'Make Private' : 'Make Public'}</button>`;
|
||
|
||
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="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;
|
||
</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 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="4" 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;
|
||
});
|
||
|
||
function editList(listId) {
|
||
const token = localStorage.getItem('access_token');
|
||
|
||
fetch(`/api/url-lists/${listId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(list => {
|
||
// 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
|
||
}));
|
||
window.location.href = '{{ url_for("create_list") }}';
|
||
});
|
||
}
|
||
|
||
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 %}
|