dashboard / index.html
JymNils's picture
el tamaño del widget se resetea al hacer una edicion o borrado de una linea, que esto no suceda y se mantenga - Initial Deployment
f506fdb verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.widget {
transition: all 0.2s ease;
position: relative;
overflow: hidden;
min-width: 250px;
min-height: 150px;
width: calc(33.333% - 20px);
flex: 0 0 calc(33.333% - 20px);
margin-bottom: 20px;
resize: vertical;
}
.widget.size-1 {
width: calc(33.333% - 20px);
flex: 0 0 calc(33.333% - 20px);
height: 150px;
}
.widget.size-2 {
width: calc(66.666% - 20px);
flex: 0 0 calc(66.666% - 20px);
height: 200px;
}
.widget.size-3 {
width: calc(100% - 20px);
flex: 0 0 calc(100% - 20px);
height: 250px;
}
.resize-controls {
position: absolute;
right: 10px;
top: 10px;
display: flex;
gap: 5px;
z-index: 10;
}
.resize-btn {
width: 20px;
height: 20px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 10px;
}
.resize-btn:hover {
background: #e0e0e0;
}
.widget:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.bookmark-item {
padding: 0.05rem 0.3rem;
margin: 0;
}
.bookmark-item:hover {
background-color: rgba(59, 130, 246, 0.1);
margin: 0;
}
.bookmark-link {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
flex-grow: 1;
padding: 0.1rem 0;
}
.sortable-ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-blue-600">My Dashboard</h1>
<div class="flex space-x-4">
<button id="addWidgetBtn" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center">
<i data-feather="plus" class="mr-2"></i> Add Widget
</button>
</div>
</header>
<!-- Widgets Container -->
<div id="widgetsContainer" class="flex flex-wrap gap-6">
<!-- Widgets will be added here -->
</div>
<!-- Add Widget Modal -->
<div id="addWidgetModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Add New Widget</h3>
<button id="closeWidgetModal" class="text-gray-500 hover:text-gray-700">
<i data-feather="x"></i>
</button>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Widget Title</label>
<input type="text" id="widgetTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex justify-end space-x-3">
<button id="cancelWidget" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">Cancel</button>
<button id="confirmWidget" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Add Widget</button>
</div>
</div>
</div>
<!-- Add Bookmark Modal -->
<div id="addBookmarkModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Add New Bookmark</h3>
<button id="closeBookmarkModal" class="text-gray-500 hover:text-gray-700">
<i data-feather="x"></i>
</button>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Title</label>
<input type="text" id="bookmarkTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">URL</label>
<input type="text" id="bookmarkUrl" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://">
</div>
<div class="flex justify-end space-x-3">
<button id="cancelBookmark" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">Cancel</button>
<button id="confirmBookmark" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Add Bookmark</button>
</div>
</div>
</div>
<!-- Edit Widget Modal -->
<div id="editWidgetModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Edit Widget</h3>
<button id="closeEditWidgetModal" class="text-gray-500 hover:text-gray-700">
<i data-feather="x"></i>
</button>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">Widget Title</label>
<input type="text" id="editWidgetTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex justify-end space-x-3">
<button id="deleteWidget" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600">Delete Widget</button>
<button id="saveWidget" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save Changes</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
// Initialize dashboard with sample widgets if empty
if (!localStorage.getItem('widgets')) {
const sampleWidgets = [
{
id: 'widget-' + Date.now(),
title: 'Favorites',
width: '300px',
height: '200px',
bookmarks: [
{ id: 'bookmark-' + Date.now() + 1, title: 'Google', url: 'https://google.com' },
{ id: 'bookmark-' + Date.now() + 2, title: 'GitHub', url: 'https://github.com' }
]
},
{
id: 'widget-' + (Date.now() + 1),
title: 'Social',
width: '300px',
height: '200px',
bookmarks: [
{ id: 'bookmark-' + (Date.now() + 3), title: 'Twitter', url: 'https://twitter.com' },
{ id: 'bookmark-' + (Date.now() + 4), title: 'Facebook', url: 'https://facebook.com' }
]
}
];
localStorage.setItem('widgets', JSON.stringify(sampleWidgets));
}
// Load widgets from localStorage
loadWidgets();
// Make widgets sortable
const widgetsContainer = document.getElementById('widgetsContainer');
new Sortable(widgetsContainer, {
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: function() {
saveWidgetsOrder();
}
});
// Modal controls
const addWidgetModal = document.getElementById('addWidgetModal');
const addBookmarkModal = document.getElementById('addBookmarkModal');
const editWidgetModal = document.getElementById('editWidgetModal');
let currentWidgetId = null;
let editMode = false;
// Show/hide add widget modal
document.getElementById('addWidgetBtn').addEventListener('click', () => {
addWidgetModal.classList.remove('hidden');
});
document.getElementById('closeWidgetModal').addEventListener('click', () => {
addWidgetModal.classList.add('hidden');
});
document.getElementById('cancelWidget').addEventListener('click', () => {
addWidgetModal.classList.add('hidden');
});
// Show/hide add bookmark modal
function showAddBookmarkModal(widgetId) {
currentWidgetId = widgetId;
// Reset modal state
document.getElementById('bookmarkTitle').value = '';
document.getElementById('bookmarkUrl').value = '';
document.querySelector('#addBookmarkModal h3').textContent = 'Add New Bookmark';
document.getElementById('confirmBookmark').textContent = 'Add Bookmark';
delete document.getElementById('confirmBookmark').dataset.bookmarkId;
addBookmarkModal.classList.remove('hidden');
}
document.getElementById('closeBookmarkModal').addEventListener('click', () => {
addBookmarkModal.classList.add('hidden');
});
document.getElementById('cancelBookmark').addEventListener('click', () => {
addBookmarkModal.classList.add('hidden');
});
// Show/hide edit widget modal
function showEditWidgetModal(widgetId) {
currentWidgetId = widgetId;
const widgets = JSON.parse(localStorage.getItem('widgets'));
const widget = widgets.find(w => w.id === widgetId);
document.getElementById('editWidgetTitle').value = widget.title;
editWidgetModal.classList.remove('hidden');
}
document.getElementById('closeEditWidgetModal').addEventListener('click', () => {
editWidgetModal.classList.add('hidden');
});
// Add new widget
document.getElementById('confirmWidget').addEventListener('click', () => {
const title = document.getElementById('widgetTitle').value.trim();
if (title) {
const newWidget = {
id: 'widget-' + Date.now(),
title: title,
bookmarks: [],
size: 1 // Default to smallest size (1/3 width)
};
const widgets = JSON.parse(localStorage.getItem('widgets')) || [];
widgets.push(newWidget);
localStorage.setItem('widgets', JSON.stringify(widgets));
loadWidgets();
addWidgetModal.classList.add('hidden');
document.getElementById('widgetTitle').value = '';
}
});
// Add or edit bookmark
document.getElementById('confirmBookmark').addEventListener('click', () => {
const title = document.getElementById('bookmarkTitle').value.trim();
let url = document.getElementById('bookmarkUrl').value.trim();
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
if (title && url) {
const widgets = JSON.parse(localStorage.getItem('widgets'));
const widgetIndex = widgets.findIndex(w => w.id === currentWidgetId);
if (widgetIndex !== -1) {
const bookmarkId = document.getElementById('confirmBookmark').dataset.bookmarkId;
if (bookmarkId) {
// Editing existing bookmark
const bookmarkIndex = widgets[widgetIndex].bookmarks.findIndex(b => b.id === bookmarkId);
if (bookmarkIndex !== -1) {
widgets[widgetIndex].bookmarks[bookmarkIndex].title = title;
widgets[widgetIndex].bookmarks[bookmarkIndex].url = url;
}
} else {
// Adding new bookmark
const newBookmark = {
id: 'bookmark-' + Date.now(),
title: title,
url: url
};
widgets[widgetIndex].bookmarks.push(newBookmark);
}
localStorage.setItem('widgets', JSON.stringify(widgets));
loadWidgets();
addBookmarkModal.classList.add('hidden');
document.getElementById('bookmarkTitle').value = '';
document.getElementById('bookmarkUrl').value = '';
// Reset modal state
document.querySelector('#addBookmarkModal h3').textContent = 'Add New Bookmark';
document.getElementById('confirmBookmark').textContent = 'Add Bookmark';
delete document.getElementById('confirmBookmark').dataset.bookmarkId;
}
}
});
// Save widget changes
document.getElementById('saveWidget').addEventListener('click', () => {
const title = document.getElementById('editWidgetTitle').value.trim();
if (title) {
const widgets = JSON.parse(localStorage.getItem('widgets'));
const widgetIndex = widgets.findIndex(w => w.id === currentWidgetId);
if (widgetIndex !== -1) {
widgets[widgetIndex].title = title;
localStorage.setItem('widgets', JSON.stringify(widgets));
loadWidgets();
editWidgetModal.classList.add('hidden');
}
}
});
// Delete widget
document.getElementById('deleteWidget').addEventListener('click', () => {
if (confirm('Are you sure you want to delete this widget and all its bookmarks?')) {
const widgets = JSON.parse(localStorage.getItem('widgets'));
const updatedWidgets = widgets.filter(w => w.id !== currentWidgetId);
localStorage.setItem('widgets', JSON.stringify(updatedWidgets));
loadWidgets();
editWidgetModal.classList.add('hidden');
}
});
// Load widgets from localStorage
function loadWidgets() {
const widgetsContainer = document.getElementById('widgetsContainer');
widgetsContainer.innerHTML = '';
const widgets = JSON.parse(localStorage.getItem('widgets')) || [];
widgets.forEach(widget => {
const widgetElement = document.createElement('div');
widgetElement.className = 'widget bg-white rounded-lg shadow-md p-4 relative';
widgetElement.dataset.widgetId = widget.id;
widgetElement.style.width = widget.width || '300px';
widgetElement.style.height = widget.height || '200px';
// Add resize controls
const resizeControls = document.createElement('div');
resizeControls.className = 'resize-controls';
// Horizontal resize buttons
const hSizes = [1, 2, 3];
hSizes.forEach(size => {
const sizeBtn = document.createElement('button');
sizeBtn.className = 'resize-btn';
sizeBtn.textContent = `${size}x`;
sizeBtn.title = `Width ${size}/3`;
sizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
resizeWidget(widget.id, size, 'width');
});
resizeControls.appendChild(sizeBtn);
});
widgetElement.appendChild(resizeControls);
// Set initial size classes
widgetElement.classList.add(`size-${widget.widthSize || 1}`);
widgetElement.classList.add(`height-${widget.heightSize || 1}`);
widgetElement.style.height = widget.height || '150px';
// Widget header
const header = document.createElement('div');
header.className = 'flex justify-between items-center mb-4';
const title = document.createElement('h2');
title.className = 'text-xl font-semibold text-gray-800';
title.textContent = widget.title;
const controls = document.createElement('div');
controls.className = 'flex space-x-2 widget-edit-controls hidden';
const editBtn = document.createElement('button');
editBtn.className = 'text-blue-500 hover:text-blue-700';
editBtn.innerHTML = '<i data-feather="edit-2" class="w-4 h-4"></i>';
editBtn.addEventListener('click', () => showEditWidgetModal(widget.id));
const addBookmarkBtn = document.createElement('button');
addBookmarkBtn.className = 'text-green-500 hover:text-green-700';
addBookmarkBtn.innerHTML = '<i data-feather="plus" class="w-4 h-4"></i>';
addBookmarkBtn.addEventListener('click', () => showAddBookmarkModal(widget.id));
controls.appendChild(addBookmarkBtn);
controls.appendChild(editBtn);
header.appendChild(title);
header.appendChild(controls);
// Bookmarks list
const bookmarksList = document.createElement('div');
bookmarksList.className = 'space-y-1';
widget.bookmarks.forEach(bookmark => {
const bookmarkItem = document.createElement('div');
bookmarkItem.className = 'bookmark-item flex justify-between items-center p-1 rounded-md transition cursor-pointer';
bookmarkItem.dataset.bookmarkId = bookmark.id;
const bookmarkLink = document.createElement('a');
bookmarkLink.className = 'bookmark-link text-blue-600 hover:text-blue-800 flex-grow';
bookmarkLink.href = bookmark.url;
bookmarkLink.target = '_blank';
// Add double click handler
bookmarkLink.addEventListener('dblclick', (e) => {
e.preventDefault();
window.open(bookmark.url, '_blank');
});
const linkIcon = document.createElement('i');
linkIcon.dataset.feather = 'external-link';
linkIcon.className = 'mr-1 w-3 h-3';
const linkText = document.createElement('span');
linkText.className = 'truncate';
linkText.textContent = bookmark.title;
linkText.title = bookmark.title;
bookmarkLink.appendChild(linkIcon);
bookmarkLink.appendChild(linkText);
const controls = document.createElement('div');
controls.className = 'flex space-x-1 ml-2';
const editBtn = document.createElement('button');
editBtn.className = 'text-gray-500 hover:text-gray-700';
editBtn.innerHTML = '<i data-feather="edit" class="w-3 h-3"></i>';
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
editBookmark(widget.id, bookmark.id);
});
const deleteBtn = document.createElement('button');
deleteBtn.className = 'text-red-500 hover:text-red-700';
deleteBtn.innerHTML = '<i data-feather="trash-2" class="w-3 h-3"></i>';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteBookmark(widget.id, bookmark.id);
});
controls.appendChild(editBtn);
controls.appendChild(deleteBtn);
bookmarkItem.appendChild(bookmarkLink);
bookmarkItem.appendChild(controls);
bookmarksList.appendChild(bookmarkItem);
});
// Add bookmark button (visible in edit mode)
const addBookmarkBtnBottom = document.createElement('button');
addBookmarkBtnBottom.className = 'absolute bottom-2 right-2 p-1 text-blue-500 hover:text-blue-700 rounded-full hover:bg-blue-50';
addBookmarkBtnBottom.innerHTML = '<i data-feather="plus" class="w-4 h-4"></i>';
addBookmarkBtnBottom.addEventListener('click', () => showAddBookmarkModal(widget.id));
widgetElement.appendChild(header);
widgetElement.appendChild(bookmarksList);
widgetElement.appendChild(addBookmarkBtnBottom);
widgetsContainer.appendChild(widgetElement);
});
feather.replace();
}
// Edit bookmark
function editBookmark(widgetId, bookmarkId) {
const widgets = JSON.parse(localStorage.getItem('widgets'));
const widgetIndex = widgets.findIndex(w => w.id === widgetId);
if (widgetIndex !== -1) {
const bookmark = widgets[widgetIndex].bookmarks.find(b => b.id === bookmarkId);
if (bookmark) {
document.getElementById('bookmarkTitle').value = bookmark.title;
document.getElementById('bookmarkUrl').value = bookmark.url;
currentWidgetId = widgetId;
// Store the bookmark ID to update
document.getElementById('confirmBookmark').dataset.bookmarkId = bookmarkId;
// Change modal title and button text
document.querySelector('#addBookmarkModal h3').textContent = 'Edit Bookmark';
document.getElementById('confirmBookmark').textContent = 'Save Changes';
addBookmarkModal.classList.remove('hidden');
}
}
}
// Delete bookmark
function deleteBookmark(widgetId, bookmarkId) {
if (confirm('Are you sure you want to delete this bookmark?')) {
const widgets = JSON.parse(localStorage.getItem('widgets'));
const widgetIndex = widgets.findIndex(w => w.id === widgetId);
if (widgetIndex !== -1) {
widgets[widgetIndex].bookmarks = widgets[widgetIndex].bookmarks.filter(b => b.id !== bookmarkId);
localStorage.setItem('widgets', JSON.stringify(widgets));
loadWidgets();
}
}
}
// Save widgets order after drag and drop
function saveWidgetsOrder() {
const widgetsContainer = document.getElementById('widgetsContainer');
const widgetElements = Array.from(widgetsContainer.children);
const widgets = JSON.parse(localStorage.getItem('widgets'));
const orderedWidgets = [];
widgetElements.forEach(element => {
const widgetId = element.dataset.widgetId;
const widget = widgets.find(w => w.id === widgetId);
if (widget) {
orderedWidgets.push(widget);
}
});
localStorage.setItem('widgets', JSON.stringify(orderedWidgets));
}
// Resize widget function
function resizeWidget(widgetId, size, dimension) {
const widgets = JSON.parse(localStorage.getItem('widgets'));
const widgetIndex = widgets.findIndex(w => w.id === widgetId);
if (widgetIndex !== -1) {
if (dimension === 'width') {
widgets[widgetIndex].widthSize = size;
} else {
widgets[widgetIndex].heightSize = size;
}
localStorage.setItem('widgets', JSON.stringify(widgets));
// Update all widgets to ensure proper layout
const widgetElements = document.querySelectorAll('.widget');
widgetElements.forEach(el => {
const id = el.dataset.widgetId;
const widget = widgets.find(w => w.id === id);
// Remove all size classes
el.className = el.className.replace(/\b(size|height)-\d\b/g, '');
// Add width size class
const widthSize = widget.widthSize || 1;
el.classList.add(`size-${widthSize}`);
// Add height size class
const heightSize = widget.heightSize || 1;
el.classList.add(`height-${heightSize}`);
// Set height based on size
if (heightSize === 1) el.style.height = '150px';
else if (heightSize === 2) el.style.height = '200px';
else if (heightSize === 3) el.style.height = '250px';
});
}
}
});
</script>
</body>
</html>