| <!DOCTYPE html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ToolLink Saver</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> |
| .tool-card:hover .tool-actions { |
| opacity: 1; |
| } |
| |
| .rating-stars input { |
| display: none; |
| } |
| |
| .rating-stars label { |
| cursor: pointer; |
| font-size: 1.5rem; |
| color: #d1d5db; |
| } |
| |
| .rating-stars input:checked ~ label { |
| color: #f59e0b; |
| } |
| |
| .rating-stars label:hover, |
| .rating-stars label:hover ~ label { |
| color: #f59e0b; |
| } |
| |
| .tag:hover { |
| transform: translateY(-2px); |
| } |
| |
| .screenshot-container { |
| background-color: #f3f4f6; |
| background-image: linear-gradient(45deg, #e5e7eb 25%, transparent 25%, transparent 75%, #e5e7eb 75%, #e5e7eb), |
| linear-gradient(45deg, #e5e7eb 25%, transparent 25%, transparent 75%, #e5e7eb 75%, #e5e7eb); |
| background-size: 20px 20px; |
| background-position: 0 0, 10px 10px; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| |
| <header class="mb-10"> |
| <div class="flex flex-col md:flex-row justify-between items-center"> |
| <div class="mb-4 md:mb-0"> |
| <h1 class="text-3xl font-bold text-indigo-700"> |
| <i class="fas fa-link mr-2"></i>ToolLink Saver |
| </h1> |
| <p class="text-gray-600">Guarda y organiza tus herramientas favoritas</p> |
| </div> |
| <button id="addToolBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2 rounded-lg flex items-center transition-colors"> |
| <i class="fas fa-plus mr-2"></i> Nueva Herramienta |
| </button> |
| </div> |
| </header> |
|
|
| |
| <main> |
| |
| <div class="bg-white rounded-lg shadow-md p-4 mb-8"> |
| <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> |
| <div class="w-full md:w-1/2"> |
| <label for="search" class="block text-sm font-medium text-gray-700 mb-1">Buscar</label> |
| <div class="relative"> |
| <input type="text" id="search" placeholder="Buscar herramientas..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
| </div> |
| </div> |
| <div class="w-full md:w-1/4"> |
| <label for="categoryFilter" class="block text-sm font-medium text-gray-700 mb-1">Categoría</label> |
| <select id="categoryFilter" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
| <option value="">Todas</option> |
| <option value="Desarrollo">Desarrollo</option> |
| <option value="Diseño">Diseño</option> |
| <option value="Productividad">Productividad</option> |
| <option value="Marketing">Marketing</option> |
| <option value="IA">IA</option> |
| </select> |
| </div> |
| <div class="w-full md:w-1/4"> |
| <label for="tagFilter" class="block text-sm font-medium text-gray-700 mb-1">Tag</label> |
| <select id="tagFilter" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
| <option value="">Todos</option> |
| |
| </select> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="toolsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
| |
| <div class="text-center py-10 text-gray-500" id="emptyState"> |
| <i class="fas fa-box-open text-5xl mb-4 text-gray-300"></i> |
| <h3 class="text-xl font-medium">No hay herramientas guardadas</h3> |
| <p class="mt-2">Comienza agregando tu primera herramienta</p> |
| </div> |
| </div> |
| </main> |
|
|
| |
| <div id="addToolModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-2xl font-bold text-gray-800">Agregar Nueva Herramienta</h2> |
| <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times text-xl"></i> |
| </button> |
| </div> |
| |
| <form id="toolForm" class="space-y-4"> |
| <div> |
| <label for="toolUrl" class="block text-sm font-medium text-gray-700 mb-1">URL de la herramienta*</label> |
| <input type="url" id="toolUrl" required 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="https://ejemplo.com"> |
| </div> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label for="toolTitle" class="block text-sm font-medium text-gray-700 mb-1">Título*</label> |
| <input type="text" id="toolTitle" required 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="Nombre de la herramienta"> |
| </div> |
| |
| <div> |
| <label for="toolCategory" class="block text-sm font-medium text-gray-700 mb-1">Categoría*</label> |
| <select id="toolCategory" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
| <option value="">Seleccionar categoría</option> |
| <option value="Desarrollo">Desarrollo</option> |
| <option value="Diseño">Diseño</option> |
| <option value="Productividad">Productividad</option> |
| <option value="Marketing">Marketing</option> |
| <option value="IA">IA</option> |
| <option value="Otros">Otros</option> |
| </select> |
| </div> |
| </div> |
| |
| <div> |
| <label for="toolDescription" class="block text-sm font-medium text-gray-700 mb-1">Descripción</label> |
| <textarea id="toolDescription" 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="Breve descripción de la herramienta..."></textarea> |
| </div> |
| |
| <div> |
| <label for="toolTags" class="block text-sm font-medium text-gray-700 mb-1">Tags (separados por comas)</label> |
| <input type="text" id="toolTags" 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="web, diseño, ui"> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Calificación</label> |
| <div class="rating-stars flex justify-center space-x-1"> |
| <input type="radio" id="star5" name="rating" value="5"> |
| <label for="star5"><i class="fas fa-star"></i></label> |
| |
| <input type="radio" id="star4" name="rating" value="4"> |
| <label for="star4"><i class="fas fa-star"></i></label> |
| |
| <input type="radio" id="star3" name="rating" value="3"> |
| <label for="star3"><i class="fas fa-star"></i></label> |
| |
| <input type="radio" id="star2" name="rating" value="2"> |
| <label for="star2"><i class="fas fa-star"></i></label> |
| |
| <input type="radio" id="star1" name="rating" value="1"> |
| <label for="star1"><i class="fas fa-star"></i></label> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Screenshot</label> |
| <div id="screenshotContainer" class="screenshot-container rounded-lg border border-gray-300 p-4 flex items-center justify-center h-48"> |
| <p class="text-gray-500 text-center">El screenshot se generará automáticamente al guardar</p> |
| </div> |
| </div> |
| |
| <div class="flex justify-end space-x-3 pt-4"> |
| <button type="button" id="cancelBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">Cancelar</button> |
| <button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 flex items-center"> |
| <i class="fas fa-save mr-2"></i> Guardar |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const addToolBtn = document.getElementById('addToolBtn'); |
| const addToolModal = document.getElementById('addToolModal'); |
| const closeModalBtn = document.getElementById('closeModalBtn'); |
| const cancelBtn = document.getElementById('cancelBtn'); |
| const toolForm = document.getElementById('toolForm'); |
| const toolsGrid = document.getElementById('toolsGrid'); |
| const emptyState = document.getElementById('emptyState'); |
| const searchInput = document.getElementById('search'); |
| const categoryFilter = document.getElementById('categoryFilter'); |
| const tagFilter = document.getElementById('tagFilter'); |
| |
| |
| let tools = JSON.parse(localStorage.getItem('tools')) || []; |
| let allTags = new Set(); |
| |
| |
| initApp(); |
| |
| |
| addToolBtn.addEventListener('click', () => addToolModal.classList.remove('hidden')); |
| closeModalBtn.addEventListener('click', () => addToolModal.classList.add('hidden')); |
| cancelBtn.addEventListener('click', () => addToolModal.classList.add('hidden')); |
| |
| toolForm.addEventListener('submit', function(e) { |
| e.preventDefault(); |
| saveTool(); |
| }); |
| |
| searchInput.addEventListener('input', filterTools); |
| categoryFilter.addEventListener('change', filterTools); |
| tagFilter.addEventListener('change', filterTools); |
| |
| |
| function initApp() { |
| if (tools.length > 0) { |
| emptyState.classList.add('hidden'); |
| renderTools(tools); |
| updateTagFilter(); |
| } |
| } |
| |
| function saveTool() { |
| const toolUrl = document.getElementById('toolUrl').value; |
| const toolTitle = document.getElementById('toolTitle').value; |
| const toolDescription = document.getElementById('toolDescription').value; |
| const toolCategory = document.getElementById('toolCategory').value; |
| const toolTags = document.getElementById('toolTags').value; |
| const rating = document.querySelector('input[name="rating"]:checked')?.value || 0; |
| |
| |
| const screenshot = `https://api.screenshotone.com/take?url=${encodeURIComponent(toolUrl)}&access_key=demo`; |
| |
| const newTool = { |
| id: Date.now(), |
| url: toolUrl, |
| title: toolTitle, |
| description: toolDescription, |
| category: toolCategory, |
| tags: toolTags.split(',').map(tag => tag.trim()).filter(tag => tag !== ''), |
| rating: parseInt(rating), |
| screenshot: screenshot, |
| createdAt: new Date().toISOString() |
| }; |
| |
| tools.unshift(newTool); |
| localStorage.setItem('tools', JSON.stringify(tools)); |
| |
| |
| if (emptyState.classList.contains('hidden')) { |
| renderTools(tools); |
| } else { |
| emptyState.classList.add('hidden'); |
| renderTools(tools); |
| } |
| |
| |
| updateTagFilter(); |
| |
| |
| toolForm.reset(); |
| addToolModal.classList.add('hidden'); |
| } |
| |
| function renderTools(toolsToRender) { |
| toolsGrid.innerHTML = ''; |
| |
| toolsToRender.forEach(tool => { |
| const toolCard = document.createElement('div'); |
| toolCard.className = 'bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow tool-card relative'; |
| |
| |
| let stars = ''; |
| for (let i = 0; i < 5; i++) { |
| if (i < tool.rating) { |
| stars += '<i class="fas fa-star text-yellow-400"></i>'; |
| } else { |
| stars += '<i class="far fa-star text-yellow-400"></i>'; |
| } |
| } |
| |
| |
| let tagsHtml = ''; |
| if (tool.tags && tool.tags.length > 0) { |
| tool.tags.forEach(tag => { |
| allTags.add(tag); |
| tagsHtml += `<span class="tag inline-block bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full mr-1 mb-1 transition-transform">${tag}</span>`; |
| }); |
| } |
| |
| toolCard.innerHTML = ` |
| <div class="screenshot-container h-40 overflow-hidden relative"> |
| <img src="${tool.screenshot}" alt="${tool.title}" class="w-full h-full object-cover"> |
| <div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent"></div> |
| </div> |
| <div class="p-4"> |
| <div class="flex justify-between items-start mb-2"> |
| <h3 class="font-bold text-lg text-gray-800 truncate">${tool.title}</h3> |
| <span class="bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full">${tool.category}</span> |
| </div> |
| <p class="text-gray-600 text-sm mb-3 line-clamp-2">${tool.description || 'Sin descripción'}</p> |
| <div class="flex items-center mb-3"> |
| ${stars} |
| <span class="text-gray-500 text-sm ml-2">${tool.rating}/5</span> |
| </div> |
| <div class="flex flex-wrap mb-2"> |
| ${tagsHtml} |
| </div> |
| <div class="flex justify-between items-center pt-2 border-t border-gray-100"> |
| <a href="${tool.url}" target="_blank" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium flex items-center"> |
| <i class="fas fa-external-link-alt mr-1"></i> Visitar |
| </a> |
| <div class="tool-actions opacity-0 transition-opacity flex space-x-2"> |
| <button class="text-gray-500 hover:text-indigo-600" onclick="editTool(${tool.id})"> |
| <i class="fas fa-edit"></i> |
| </button> |
| <button class="text-gray-500 hover:text-red-600" onclick="deleteTool(${tool.id})"> |
| <i class="fas fa-trash"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| toolsGrid.appendChild(toolCard); |
| }); |
| } |
| |
| function updateTagFilter() { |
| tagFilter.innerHTML = '<option value="">Todos</option>'; |
| allTags.forEach(tag => { |
| const option = document.createElement('option'); |
| option.value = tag; |
| option.textContent = tag; |
| tagFilter.appendChild(option); |
| }); |
| } |
| |
| function filterTools() { |
| const searchTerm = searchInput.value.toLowerCase(); |
| const selectedCategory = categoryFilter.value; |
| const selectedTag = tagFilter.value; |
| |
| const filteredTools = tools.filter(tool => { |
| const matchesSearch = tool.title.toLowerCase().includes(searchTerm) || |
| tool.description.toLowerCase().includes(searchTerm); |
| const matchesCategory = selectedCategory === '' || tool.category === selectedCategory; |
| const matchesTag = selectedTag === '' || (tool.tags && tool.tags.includes(selectedTag)); |
| |
| return matchesSearch && matchesCategory && matchesTag; |
| }); |
| |
| renderTools(filteredTools); |
| |
| if (filteredTools.length === 0) { |
| emptyState.classList.remove('hidden'); |
| } else { |
| emptyState.classList.add('hidden'); |
| } |
| } |
| |
| |
| window.editTool = function(id) { |
| const tool = tools.find(t => t.id === id); |
| if (tool) { |
| document.getElementById('toolUrl').value = tool.url; |
| document.getElementById('toolTitle').value = tool.title; |
| document.getElementById('toolDescription').value = tool.description || ''; |
| document.getElementById('toolCategory').value = tool.category; |
| document.getElementById('toolTags').value = tool.tags.join(', '); |
| |
| |
| if (tool.rating > 0) { |
| document.getElementById(`star${tool.rating}`).checked = true; |
| } |
| |
| |
| const screenshotContainer = document.getElementById('screenshotContainer'); |
| screenshotContainer.innerHTML = `<img src="${tool.screenshot}" alt="${tool.title}" class="max-h-full max-w-full">`; |
| |
| |
| toolForm.dataset.editingId = id; |
| document.querySelector('#addToolModal h2').textContent = 'Editar Herramienta'; |
| |
| addToolModal.classList.remove('hidden'); |
| } |
| }; |
| |
| window.deleteTool = function(id) { |
| if (confirm('¿Estás seguro de que quieres eliminar esta herramienta?')) { |
| tools = tools.filter(tool => tool.id !== id); |
| localStorage.setItem('tools', JSON.stringify(tools)); |
| |
| if (tools.length === 0) { |
| emptyState.classList.remove('hidden'); |
| toolsGrid.innerHTML = ''; |
| } else { |
| renderTools(tools); |
| } |
| |
| updateTagFilter(); |
| } |
| }; |
| }); |
| </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=ARX88/toolvault" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |