linklist / index.html
kyupark's picture
app that manages multiple lists of links - Initial Deployment
16ae373 verified
Raw
History Blame Contribute Delete
33.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LinkList - Manage Your Links</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.link-card:hover .link-actions {
opacity: 1;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="flex justify-between items-center mb-8">
<div class="flex items-center">
<i class="fas fa-link text-indigo-600 text-3xl mr-3"></i>
<h1 class="text-2xl font-bold text-gray-800">LinkList</h1>
</div>
<button id="newListBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-plus mr-2"></i> New List
</button>
</header>
<!-- Main Content -->
<div class="flex flex-col lg:flex-row gap-6">
<!-- Sidebar - Lists -->
<div class="w-full lg:w-1/4 bg-white rounded-xl shadow-sm p-4 h-fit">
<h2 class="font-semibold text-lg text-gray-700 mb-4 flex items-center">
<i class="fas fa-list mr-2 text-indigo-500"></i> Your Lists
</h2>
<div id="listsContainer" class="space-y-2 max-h-96 overflow-y-auto custom-scrollbar">
<!-- Lists will be added here dynamically -->
<div class="p-3 bg-indigo-50 rounded-lg border border-indigo-100">
<div class="flex justify-between items-center">
<span class="font-medium text-indigo-700">Quick Access</span>
<span class="text-xs bg-indigo-100 text-indigo-800 px-2 py-1 rounded-full">3 links</span>
</div>
</div>
</div>
</div>
<!-- Main Area - Links -->
<div class="w-full lg:w-3/4">
<!-- Current List Header -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
<div class="flex justify-between items-center">
<div>
<h2 id="currentListName" class="font-semibold text-xl text-gray-800">Quick Access</h2>
<p id="currentListDesc" class="text-sm text-gray-500">Your frequently visited links</p>
</div>
<div class="flex space-x-2">
<button id="editListBtn" class="p-2 text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 rounded-full">
<i class="fas fa-pencil-alt"></i>
</button>
<button id="deleteListBtn" class="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-full">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
<!-- Add New Link -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
<div class="flex items-center space-x-3">
<div class="flex-1">
<input id="newLinkUrl" type="url" placeholder="https://example.com" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<button id="addLinkBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg">
Add Link
</button>
</div>
<div id="linkDetailsForm" class="mt-4 hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input id="linkTitle" type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
<input id="linkCategory" type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea id="linkDescription" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea>
</div>
</div>
<div class="mt-4 flex justify-end space-x-3">
<button id="cancelLinkBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">Cancel</button>
<button id="saveLinkBtn" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">Save Link</button>
</div>
</div>
</div>
<!-- Links Grid -->
<div id="linksContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Link cards will be added here dynamically -->
<div class="link-card bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100 hover:shadow-md transition-all duration-200 fade-in">
<div class="p-4">
<div class="flex items-start">
<div class="bg-indigo-100 p-3 rounded-lg mr-3">
<i class="fas fa-globe text-indigo-600"></i>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-800 truncate">Google</h3>
<p class="text-sm text-gray-500 truncate">Search engine</p>
<a href="https://google.com" target="_blank" class="text-xs text-indigo-600 hover:underline block mt-1 truncate">https://google.com</a>
</div>
</div>
</div>
<div class="link-actions bg-gray-50 px-4 py-2 border-t border-gray-100 flex justify-end opacity-0 transition-opacity duration-200">
<button class="p-1 text-gray-400 hover:text-indigo-600">
<i class="fas fa-pencil-alt text-sm"></i>
</button>
<button class="p-1 text-gray-400 hover:text-red-600 ml-2">
<i class="fas fa-trash-alt text-sm"></i>
</button>
</div>
</div>
<div class="link-card bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100 hover:shadow-md transition-all duration-200 fade-in">
<div class="p-4">
<div class="flex items-start">
<div class="bg-blue-100 p-3 rounded-lg mr-3">
<i class="fab fa-twitter text-blue-500"></i>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-800 truncate">Twitter</h3>
<p class="text-sm text-gray-500 truncate">Social media platform</p>
<a href="https://twitter.com" target="_blank" class="text-xs text-indigo-600 hover:underline block mt-1 truncate">https://twitter.com</a>
</div>
</div>
</div>
<div class="link-actions bg-gray-50 px-4 py-2 border-t border-gray-100 flex justify-end opacity-0 transition-opacity duration-200">
<button class="p-1 text-gray-400 hover:text-indigo-600">
<i class="fas fa-pencil-alt text-sm"></i>
</button>
<button class="p-1 text-gray-400 hover:text-red-600 ml-2">
<i class="fas fa-trash-alt text-sm"></i>
</button>
</div>
</div>
<div class="link-card bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100 hover:shadow-md transition-all duration-200 fade-in">
<div class="p-4">
<div class="flex items-start">
<div class="bg-red-100 p-3 rounded-lg mr-3">
<i class="fab fa-youtube text-red-600"></i>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-800 truncate">YouTube</h3>
<p class="text-sm text-gray-500 truncate">Video sharing platform</p>
<a href="https://youtube.com" target="_blank" class="text-xs text-indigo-600 hover:underline block mt-1 truncate">https://youtube.com</a>
</div>
</div>
</div>
<div class="link-actions bg-gray-50 px-4 py-2 border-t border-gray-100 flex justify-end opacity-0 transition-opacity duration-200">
<button class="p-1 text-gray-400 hover:text-indigo-600">
<i class="fas fa-pencil-alt text-sm"></i>
</button>
<button class="p-1 text-gray-400 hover:text-red-600 ml-2">
<i class="fas fa-trash-alt text-sm"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- New List Modal -->
<div id="newListModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-xl p-6 w-full max-w-md fade-in">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">Create New List</h3>
<button id="closeNewListModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">List Name</label>
<input id="newListName" type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="e.g. Work Resources">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea id="newListDesc" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Brief description of this list"></textarea>
</div>
<div class="flex justify-end space-x-3 pt-2">
<button id="cancelNewList" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">Cancel</button>
<button id="saveNewList" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">Create List</button>
</div>
</div>
</div>
</div>
<!-- Edit List Modal -->
<div id="editListModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-xl p-6 w-full max-w-md fade-in">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">Edit List</h3>
<button id="closeEditListModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">List Name</label>
<input id="editListName" type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea id="editListDesc" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea>
</div>
<div class="flex justify-end space-x-3 pt-2">
<button id="cancelEditList" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">Cancel</button>
<button id="saveEditList" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-xl p-6 w-full max-w-md fade-in">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">Confirm Deletion</h3>
<button id="closeDeleteModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<p class="text-gray-700">Are you sure you want to delete this list? All links in this list will also be deleted. This action cannot be undone.</p>
<div class="flex justify-end space-x-3 pt-4">
<button id="cancelDelete" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">Cancel</button>
<button id="confirmDelete" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">Delete</button>
</div>
</div>
</div>
</div>
<script>
// DOM Elements
const newListBtn = document.getElementById('newListBtn');
const newListModal = document.getElementById('newListModal');
const closeNewListModal = document.getElementById('closeNewListModal');
const cancelNewList = document.getElementById('cancelNewList');
const saveNewList = document.getElementById('saveNewList');
const newListName = document.getElementById('newListName');
const newListDesc = document.getElementById('newListDesc');
const editListBtn = document.getElementById('editListBtn');
const editListModal = document.getElementById('editListModal');
const closeEditListModal = document.getElementById('closeEditListModal');
const cancelEditList = document.getElementById('cancelEditList');
const saveEditList = document.getElementById('saveEditList');
const editListName = document.getElementById('editListName');
const editListDesc = document.getElementById('editListDesc');
const deleteListBtn = document.getElementById('deleteListBtn');
const deleteModal = document.getElementById('deleteModal');
const closeDeleteModal = document.getElementById('closeDeleteModal');
const cancelDelete = document.getElementById('cancelDelete');
const confirmDelete = document.getElementById('confirmDelete');
const addLinkBtn = document.getElementById('addLinkBtn');
const newLinkUrl = document.getElementById('newLinkUrl');
const linkDetailsForm = document.getElementById('linkDetailsForm');
const cancelLinkBtn = document.getElementById('cancelLinkBtn');
const saveLinkBtn = document.getElementById('saveLinkBtn');
const linkTitle = document.getElementById('linkTitle');
const linkCategory = document.getElementById('linkCategory');
const linkDescription = document.getElementById('linkDescription');
const listsContainer = document.getElementById('listsContainer');
const linksContainer = document.getElementById('linksContainer');
const currentListName = document.getElementById('currentListName');
const currentListDesc = document.getElementById('currentListDesc');
// State
let lists = [
{
id: 'list-1',
name: 'Quick Access',
description: 'Your frequently visited links',
links: [
{
id: 'link-1',
title: 'Google',
url: 'https://google.com',
description: 'Search engine',
category: 'Search',
icon: 'globe'
},
{
id: 'link-2',
title: 'Twitter',
url: 'https://twitter.com',
description: 'Social media platform',
category: 'Social',
icon: 'twitter'
},
{
id: 'link-3',
title: 'YouTube',
url: 'https://youtube.com',
description: 'Video sharing platform',
category: 'Entertainment',
icon: 'youtube'
}
]
}
];
let currentListId = 'list-1';
// Initialize the app
function init() {
renderLists();
renderLinks(currentListId);
// Set event listeners
newListBtn.addEventListener('click', () => newListModal.classList.remove('hidden'));
closeNewListModal.addEventListener('click', () => newListModal.classList.add('hidden'));
cancelNewList.addEventListener('click', () => newListModal.classList.add('hidden'));
saveNewList.addEventListener('click', createNewList);
editListBtn.addEventListener('click', openEditListModal);
closeEditListModal.addEventListener('click', () => editListModal.classList.add('hidden'));
cancelEditList.addEventListener('click', () => editListModal.classList.add('hidden'));
saveEditList.addEventListener('click', saveListChanges);
deleteListBtn.addEventListener('click', () => deleteModal.classList.remove('hidden'));
closeDeleteModal.addEventListener('click', () => deleteModal.classList.add('hidden'));
cancelDelete.addEventListener('click', () => deleteModal.classList.add('hidden'));
confirmDelete.addEventListener('click', deleteCurrentList);
addLinkBtn.addEventListener('click', toggleLinkForm);
cancelLinkBtn.addEventListener('click', toggleLinkForm);
saveLinkBtn.addEventListener('click', addNewLink);
}
// Render all lists in the sidebar
function renderLists() {
listsContainer.innerHTML = '';
lists.forEach(list => {
const listElement = document.createElement('div');
listElement.className = `p-3 rounded-lg border cursor-pointer hover:bg-gray-50 transition-colors ${list.id === currentListId ? 'bg-indigo-50 border-indigo-100' : 'bg-white border-gray-200'}`;
listElement.innerHTML = `
<div class="flex justify-between items-center">
<span class="font-medium ${list.id === currentListId ? 'text-indigo-700' : 'text-gray-700'}">${list.name}</span>
<span class="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded-full">${list.links.length} links</span>
</div>
`;
listElement.addEventListener('click', () => {
currentListId = list.id;
renderLists();
renderLinks(currentListId);
updateCurrentListHeader();
});
listsContainer.appendChild(listElement);
});
}
// Render links for the current list
function renderLinks(listId) {
linksContainer.innerHTML = '';
const currentList = lists.find(list => list.id === listId);
if (!currentList) return;
currentList.links.forEach(link => {
const linkCard = document.createElement('div');
linkCard.className = 'link-card bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100 hover:shadow-md transition-all duration-200 fade-in';
// Determine icon color based on category
let iconColor = 'indigo';
let iconClass = 'fas fa-globe';
if (link.icon === 'twitter') {
iconColor = 'blue';
iconClass = 'fab fa-twitter';
} else if (link.icon === 'youtube') {
iconColor = 'red';
iconClass = 'fab fa-youtube';
} else if (link.icon === 'github') {
iconColor = 'gray';
iconClass = 'fab fa-github';
}
linkCard.innerHTML = `
<div class="p-4">
<div class="flex items-start">
<div class="bg-${iconColor}-100 p-3 rounded-lg mr-3">
<i class="${iconClass} text-${iconColor}-600"></i>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-gray-800 truncate">${link.title}</h3>
<p class="text-sm text-gray-500 truncate">${link.description}</p>
<a href="${link.url}" target="_blank" class="text-xs text-indigo-600 hover:underline block mt-1 truncate">${link.url}</a>
</div>
</div>
</div>
<div class="link-actions bg-gray-50 px-4 py-2 border-t border-gray-100 flex justify-end opacity-0 transition-opacity duration-200">
<button class="p-1 text-gray-400 hover:text-indigo-600 edit-link" data-id="${link.id}">
<i class="fas fa-pencil-alt text-sm"></i>
</button>
<button class="p-1 text-gray-400 hover:text-red-600 ml-2 delete-link" data-id="${link.id}">
<i class="fas fa-trash-alt text-sm"></i>
</button>
</div>
`;
linksContainer.appendChild(linkCard);
});
// Add event listeners for edit and delete buttons
document.querySelectorAll('.edit-link').forEach(btn => {
btn.addEventListener('click', (e) => {
const linkId = e.currentTarget.getAttribute('data-id');
editLink(linkId);
});
});
document.querySelectorAll('.delete-link').forEach(btn => {
btn.addEventListener('click', (e) => {
const linkId = e.currentTarget.getAttribute('data-id');
deleteLink(linkId);
});
});
}
// Update the current list header
function updateCurrentListHeader() {
const currentList = lists.find(list => list.id === currentListId);
if (currentList) {
currentListName.textContent = currentList.name;
currentListDesc.textContent = currentList.description;
}
}
// Create a new list
function createNewList() {
const name = newListName.value.trim();
const description = newListDesc.value.trim();
if (!name) {
alert('Please enter a list name');
return;
}
const newList = {
id: 'list-' + Date.now(),
name,
description,
links: []
};
lists.push(newList);
newListName.value = '';
newListDesc.value = '';
newListModal.classList.add('hidden');
currentListId = newList.id;
renderLists();
renderLinks(currentListId);
updateCurrentListHeader();
}
// Open edit list modal
function openEditListModal() {
const currentList = lists.find(list => list.id === currentListId);
if (currentList) {
editListName.value = currentList.name;
editListDesc.value = currentList.description;
editListModal.classList.remove('hidden');
}
}
// Save list changes
function saveListChanges() {
const name = editListName.value.trim();
const description = editListDesc.value.trim();
if (!name) {
alert('Please enter a list name');
return;
}
const listIndex = lists.findIndex(list => list.id === currentListId);
if (listIndex !== -1) {
lists[listIndex].name = name;
lists[listIndex].description = description;
editListModal.classList.add('hidden');
renderLists();
updateCurrentListHeader();
}
}
// Delete current list
function deleteCurrentList() {
if (lists.length <= 1) {
alert('You must have at least one list');
deleteModal.classList.add('hidden');
return;
}
lists = lists.filter(list => list.id !== currentListId);
currentListId = lists[0].id;
deleteModal.classList.add('hidden');
renderLists();
renderLinks(currentListId);
updateCurrentListHeader();
}
// Toggle link form visibility
function toggleLinkForm() {
if (linkDetailsForm.classList.contains('hidden')) {
const url = newLinkUrl.value.trim();
if (!url) {
alert('Please enter a URL first');
return;
}
// Try to extract title from URL
let title = '';
try {
const urlObj = new URL(url);
title = urlObj.hostname.replace('www.', '').split('.')[0];
title = title.charAt(0).toUpperCase() + title.slice(1);
} catch (e) {
title = 'New Link';
}
linkTitle.value = title;
linkCategory.value = '';
linkDescription.value = '';
linkDetailsForm.classList.remove('hidden');
} else {
linkDetailsForm.classList.add('hidden');
}
}
// Add new link
function addNewLink() {
const url = newLinkUrl.value.trim();
const title = linkTitle.value.trim() || 'Untitled Link';
const category = linkCategory.value.trim() || 'General';
const description = linkDescription.value.trim() || 'No description';
if (!url) {
alert('Please enter a URL');
return;
}
// Extract domain for icon
let icon = 'globe';
try {
const domain = new URL(url).hostname;
if (domain.includes('twitter')) icon = 'twitter';
else if (domain.includes('youtube')) icon = 'youtube';
else if (domain.includes('github')) icon = 'github';
} catch (e) {
console.error('Invalid URL');
}
const newLink = {
id: 'link-' + Date.now(),
title,
url,
description,
category,
icon
};
const listIndex = lists.findIndex(list => list.id === currentListId);
if (listIndex !== -1) {
lists[listIndex].links.push(newLink);
newLinkUrl.value = '';
linkDetailsForm.classList.add('hidden');
renderLinks(currentListId);
}
}
// Edit a link
function editLink(linkId) {
const currentList = lists.find(list => list.id === currentListId);
if (!currentList) return;
const link = currentList.links.find(l => l.id === linkId);
if (!link) return;
// For simplicity, we'll just delete and re-add with edited values
deleteLink(linkId);
newLinkUrl.value = link.url;
linkTitle.value = link.title;
linkCategory.value = link.category;
linkDescription.value = link.description;
linkDetailsForm.classList.remove('hidden');
}
// Delete a link
function deleteLink(linkId) {
if (!confirm('Are you sure you want to delete this link?')) return;
const listIndex = lists.findIndex(list => list.id === currentListId);
if (listIndex !== -1) {
lists[listIndex].links = lists[listIndex].links.filter(link => link.id !== linkId);
renderLinks(currentListId);
}
}
// Initialize the app
document.addEventListener('DOMContentLoaded', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=kyupark/linklist" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>