|
68 | 68 | <h1 class="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent mb-2"> |
69 | 69 | Deepr |
70 | 70 | </h1> |
71 | | - <p class="text-gray-600 text-lg">Your Personal Link Manager</p> |
| 71 | + <p class="text-gray-600 text-lg mb-4">Your Personal Link Manager</p> |
| 72 | + |
| 73 | + <!-- Profile Selector --> |
| 74 | + <div class="flex items-center justify-center gap-3"> |
| 75 | + <label for="profileSelect" class="text-sm font-medium text-gray-700">Profile:</label> |
| 76 | + <select |
| 77 | + id="profileSelect" |
| 78 | + class="px-4 py-2 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 text-gray-800 bg-white" |
| 79 | + > |
| 80 | + <option value="1">Default</option> |
| 81 | + </select> |
| 82 | + <button |
| 83 | + id="addProfileBtn" |
| 84 | + class="bg-gradient-to-r from-primary-500 to-secondary-500 hover:from-primary-600 hover:to-secondary-600 text-white px-4 py-2 rounded-xl font-medium transition-all duration-200 transform hover:scale-105 flex items-center gap-1" |
| 85 | + title="Add New Profile" |
| 86 | + > |
| 87 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 88 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path> |
| 89 | + </svg> |
| 90 | + New Profile |
| 91 | + </button> |
| 92 | + </div> |
72 | 93 | </div> |
73 | 94 | </header> |
74 | 95 |
|
@@ -265,11 +286,51 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Loading links...</h3> |
265 | 286 | </div> |
266 | 287 | </div> |
267 | 288 |
|
| 289 | + <!-- Add Profile Modal --> |
| 290 | + <div id="addProfileModal" class="fixed inset-0 z-50 hidden"> |
| 291 | + <div class="absolute inset-0 bg-black bg-opacity-50" onclick="closeAddProfileModal()"></div> |
| 292 | + <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-2xl shadow-2xl p-6 w-full max-w-md"> |
| 293 | + <h3 class="text-xl font-semibold text-gray-800 mb-4">Create New Profile</h3> |
| 294 | + <form id="addProfileForm"> |
| 295 | + <div class="mb-4"> |
| 296 | + <label for="newProfileName" class="block text-sm font-medium text-gray-700 mb-2"> |
| 297 | + Profile Name |
| 298 | + </label> |
| 299 | + <input |
| 300 | + type="text" |
| 301 | + id="newProfileName" |
| 302 | + placeholder="Enter profile name..." |
| 303 | + required |
| 304 | + class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 text-gray-800" |
| 305 | + > |
| 306 | + </div> |
| 307 | + <div class="flex gap-3 justify-end"> |
| 308 | + <button |
| 309 | + type="button" |
| 310 | + onclick="closeAddProfileModal()" |
| 311 | + class="px-6 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-xl transition-all duration-200" |
| 312 | + > |
| 313 | + Cancel |
| 314 | + </button> |
| 315 | + <button |
| 316 | + type="submit" |
| 317 | + id="createProfileBtn" |
| 318 | + class="bg-gradient-to-r from-primary-500 to-secondary-500 hover:from-primary-600 hover:to-secondary-600 text-white px-6 py-2 rounded-xl font-semibold transition-all duration-200 transform hover:scale-105" |
| 319 | + > |
| 320 | + Create Profile |
| 321 | + </button> |
| 322 | + </div> |
| 323 | + </form> |
| 324 | + </div> |
| 325 | + </div> |
| 326 | + |
268 | 327 | <script> |
269 | 328 | // Global variables to store all links and available tags |
270 | 329 | let allLinks = []; |
271 | 330 | let availableTags = []; // Now contains {id, name, count} objects |
272 | 331 | let selectedTags = []; // Now contains {id, name} objects |
| 332 | + let profiles = []; // Contains {id, name, createdAt} objects |
| 333 | + let selectedProfileId = 1; // Default profile |
273 | 334 |
|
274 | 335 | // Toast notification system |
275 | 336 | function showToast(message, type = 'success') { |
@@ -320,6 +381,103 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Loading links...</h3> |
320 | 381 | } |
321 | 382 | } |
322 | 383 |
|
| 384 | + // Load profiles from API |
| 385 | + async function loadProfiles() { |
| 386 | + try { |
| 387 | + const response = await fetch('/api/profiles'); |
| 388 | + profiles = await response.json(); |
| 389 | + populateProfileSelect(); |
| 390 | + } catch (error) { |
| 391 | + console.error('Error loading profiles:', error); |
| 392 | + profiles = []; |
| 393 | + } |
| 394 | + } |
| 395 | + |
| 396 | + // Populate profile dropdown |
| 397 | + function populateProfileSelect() { |
| 398 | + const profileSelect = document.getElementById('profileSelect'); |
| 399 | + profileSelect.innerHTML = profiles.map(profile => ` |
| 400 | + <option value="${profile.id}" ${profile.id === selectedProfileId ? 'selected' : ''}> |
| 401 | + ${escapeHtml(profile.name)} |
| 402 | + </option> |
| 403 | + `).join(''); |
| 404 | + } |
| 405 | + |
| 406 | + // Handle profile selection change |
| 407 | + function onProfileChange() { |
| 408 | + const profileSelect = document.getElementById('profileSelect'); |
| 409 | + selectedProfileId = parseInt(profileSelect.value, 10); |
| 410 | + loadLinks(); |
| 411 | + showToast(`Switched to profile: ${profiles.find(p => p.id === selectedProfileId)?.name || 'Unknown'}`, 'info'); |
| 412 | + } |
| 413 | + |
| 414 | + // Open add profile modal |
| 415 | + function openAddProfileModal() { |
| 416 | + document.getElementById('addProfileModal').classList.remove('hidden'); |
| 417 | + document.getElementById('newProfileName').focus(); |
| 418 | + } |
| 419 | + |
| 420 | + // Close add profile modal |
| 421 | + function closeAddProfileModal() { |
| 422 | + document.getElementById('addProfileModal').classList.add('hidden'); |
| 423 | + document.getElementById('newProfileName').value = ''; |
| 424 | + } |
| 425 | + |
| 426 | + // Create new profile |
| 427 | + async function createProfile(event) { |
| 428 | + event.preventDefault(); |
| 429 | + |
| 430 | + const nameInput = document.getElementById('newProfileName'); |
| 431 | + const button = document.getElementById('createProfileBtn'); |
| 432 | + const name = nameInput.value.trim(); |
| 433 | + |
| 434 | + if (!name) { |
| 435 | + showToast('Please enter a profile name', 'error'); |
| 436 | + return; |
| 437 | + } |
| 438 | + |
| 439 | + const originalText = button.textContent; |
| 440 | + button.innerHTML = ` |
| 441 | + <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline" fill="none" viewBox="0 0 24 24"> |
| 442 | + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
| 443 | + <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
| 444 | + </svg> |
| 445 | + Creating... |
| 446 | + `; |
| 447 | + button.disabled = true; |
| 448 | + |
| 449 | + try { |
| 450 | + const response = await fetch('/api/profiles', { |
| 451 | + method: 'POST', |
| 452 | + headers: { |
| 453 | + 'Content-Type': 'application/json', |
| 454 | + }, |
| 455 | + body: JSON.stringify({ name }) |
| 456 | + }); |
| 457 | + |
| 458 | + if (response.ok) { |
| 459 | + showToast(`Profile "${name}" created successfully!`, 'success'); |
| 460 | + closeAddProfileModal(); |
| 461 | + await loadProfiles(); |
| 462 | + // Select the newly created profile |
| 463 | + const newProfile = profiles.find(p => p.name === name); |
| 464 | + if (newProfile) { |
| 465 | + selectedProfileId = newProfile.id; |
| 466 | + populateProfileSelect(); |
| 467 | + loadLinks(); |
| 468 | + } |
| 469 | + } else { |
| 470 | + throw new Error('Failed to create profile'); |
| 471 | + } |
| 472 | + } catch (error) { |
| 473 | + console.error('Error creating profile:', error); |
| 474 | + showToast('Failed to create profile. Please try again.', 'error'); |
| 475 | + } finally { |
| 476 | + button.textContent = originalText; |
| 477 | + button.disabled = false; |
| 478 | + } |
| 479 | + } |
| 480 | + |
323 | 481 | // Tag management functions |
324 | 482 | function addSelectedTag(tagName) { |
325 | 483 | // Check if tag is already selected |
@@ -612,7 +770,7 @@ <h3 class="text-lg font-semibold text-gray-800 line-clamp-2 flex-1"> |
612 | 770 |
|
613 | 771 | async function loadLinks() { |
614 | 772 | try { |
615 | | - const response = await fetch('/api/links'); |
| 773 | + const response = await fetch(`/api/links?profileId=${selectedProfileId}`); |
616 | 774 | const links = await response.json(); |
617 | 775 |
|
618 | 776 | allLinks = links; |
@@ -665,7 +823,8 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Error loading links</h3> |
665 | 823 | link: urlInput.value, |
666 | 824 | name: nameInput.value, |
667 | 825 | notes: notesInput.value, |
668 | | - tags: selectedTags // Send tags in {id, name} format |
| 826 | + tags: selectedTags, // Send tags in {id, name} format |
| 827 | + profileId: selectedProfileId |
669 | 828 | }) |
670 | 829 | }); |
671 | 830 |
|
@@ -738,6 +897,11 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Error loading links</h3> |
738 | 897 | document.getElementById('sortOrder').addEventListener('click', toggleSortOrder); |
739 | 898 | document.getElementById('clearFilters').addEventListener('click', clearAllFilters); |
740 | 899 |
|
| 900 | + // Profile controls |
| 901 | + document.getElementById('profileSelect').addEventListener('change', onProfileChange); |
| 902 | + document.getElementById('addProfileBtn').addEventListener('click', openAddProfileModal); |
| 903 | + document.getElementById('addProfileForm').addEventListener('submit', createProfile); |
| 904 | + |
741 | 905 | // Tag input functionality |
742 | 906 | const tagInput = document.getElementById('linkTags'); |
743 | 907 | const debouncedShowSuggestions = debounce((input) => showSuggestions(input), 200); |
@@ -768,7 +932,9 @@ <h3 class="text-xl font-semibold text-gray-700 mb-2">Error loading links</h3> |
768 | 932 |
|
769 | 933 | // Initialize |
770 | 934 | updateSortOrderIcon(); |
771 | | - loadLinks(); |
| 935 | + loadProfiles().then(() => { |
| 936 | + loadLinks(); |
| 937 | + }); |
772 | 938 | loadAvailableTags(); |
773 | 939 | }); |
774 | 940 | </script> |
|
0 commit comments