| <!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 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> |
|
|
| |
| <div id="widgetsContainer" class="flex flex-wrap gap-6"> |
| |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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(); |
| |
| |
| 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)); |
| } |
| |
| |
| loadWidgets(); |
| |
| |
| const widgetsContainer = document.getElementById('widgetsContainer'); |
| new Sortable(widgetsContainer, { |
| animation: 150, |
| ghostClass: 'sortable-ghost', |
| onEnd: function() { |
| saveWidgetsOrder(); |
| } |
| }); |
| |
| |
| const addWidgetModal = document.getElementById('addWidgetModal'); |
| const addBookmarkModal = document.getElementById('addBookmarkModal'); |
| const editWidgetModal = document.getElementById('editWidgetModal'); |
| let currentWidgetId = null; |
| let editMode = false; |
| |
| |
| 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'); |
| }); |
| |
| |
| function showAddBookmarkModal(widgetId) { |
| currentWidgetId = widgetId; |
| |
| 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'); |
| }); |
| |
| |
| 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'); |
| }); |
| |
| |
| 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 |
| }; |
| |
| 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 = ''; |
| } |
| }); |
| |
| |
| 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) { |
| |
| 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 { |
| |
| 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 = ''; |
| |
| |
| document.querySelector('#addBookmarkModal h3').textContent = 'Add New Bookmark'; |
| document.getElementById('confirmBookmark').textContent = 'Add Bookmark'; |
| delete document.getElementById('confirmBookmark').dataset.bookmarkId; |
| } |
| } |
| }); |
| |
| |
| 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'); |
| } |
| } |
| }); |
| |
| |
| 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'); |
| } |
| }); |
| |
| |
| 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'; |
| |
| |
| const resizeControls = document.createElement('div'); |
| resizeControls.className = 'resize-controls'; |
| |
| |
| 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); |
| |
| |
| widgetElement.classList.add(`size-${widget.widthSize || 1}`); |
| widgetElement.classList.add(`height-${widget.heightSize || 1}`); |
| widgetElement.style.height = widget.height || '150px'; |
| |
| |
| 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); |
| |
| |
| 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'; |
| |
| 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); |
| }); |
| |
| |
| 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(); |
| } |
| |
| |
| 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; |
| |
| |
| document.getElementById('confirmBookmark').dataset.bookmarkId = bookmarkId; |
| |
| |
| document.querySelector('#addBookmarkModal h3').textContent = 'Edit Bookmark'; |
| document.getElementById('confirmBookmark').textContent = 'Save Changes'; |
| |
| addBookmarkModal.classList.remove('hidden'); |
| } |
| } |
| } |
| |
| |
| 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(); |
| } |
| } |
| } |
| |
| |
| 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)); |
| } |
| |
| |
| 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)); |
| |
| |
| const widgetElements = document.querySelectorAll('.widget'); |
| widgetElements.forEach(el => { |
| const id = el.dataset.widgetId; |
| const widget = widgets.find(w => w.id === id); |
| |
| |
| el.className = el.className.replace(/\b(size|height)-\d\b/g, ''); |
| |
| |
| const widthSize = widget.widthSize || 1; |
| el.classList.add(`size-${widthSize}`); |
| |
| |
| const heightSize = widget.heightSize || 1; |
| el.classList.add(`height-${heightSize}`); |
| |
| |
| 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> |
|
|