experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(settings): add profiles section to settings UI

- Add renderProfilesSettings() function to display profiles
- Show current active profile
- List all profiles with radio buttons for switching
- Add Profile button with name input dialog
- Delete profile button (disabled for default/active)
- Per-profile sync configuration UI (enable/disable sync)
- Add profiles navigation item between Sync and Themes sections

Phase 6 complete.

+496
+496
app/settings/settings.js
··· 1584 1584 return feature ? feature.enabled : false; 1585 1585 }; 1586 1586 1587 + // Render profiles settings 1588 + const renderProfilesSettings = async () => { 1589 + const container = document.createElement('div'); 1590 + 1591 + // Get current profile and all profiles 1592 + let currentProfile = null; 1593 + let profiles = []; 1594 + 1595 + try { 1596 + const currentResult = await api.profiles.getCurrent(); 1597 + if (currentResult.success && currentResult.data) { 1598 + currentProfile = currentResult.data; 1599 + } 1600 + 1601 + const listResult = await api.profiles.list(); 1602 + if (listResult.success && listResult.data) { 1603 + profiles = listResult.data; 1604 + } 1605 + } catch (err) { 1606 + console.error('[settings] Failed to load profiles:', err); 1607 + } 1608 + 1609 + // Current profile section 1610 + const currentSection = document.createElement('div'); 1611 + currentSection.className = 'form-section'; 1612 + 1613 + const currentTitle = document.createElement('h3'); 1614 + currentTitle.className = 'form-section-title'; 1615 + currentTitle.textContent = 'Current Profile'; 1616 + currentSection.appendChild(currentTitle); 1617 + 1618 + const currentName = document.createElement('p'); 1619 + currentName.textContent = currentProfile ? currentProfile.name : 'Unknown'; 1620 + currentName.style.cssText = 'margin: 8px 0; font-weight: 500;'; 1621 + currentSection.appendChild(currentName); 1622 + 1623 + container.appendChild(currentSection); 1624 + 1625 + // Add Profile button 1626 + const addSection = document.createElement('div'); 1627 + addSection.className = 'form-section'; 1628 + addSection.style.cssText = 'border-top: 1px solid var(--border-primary); padding-top: 16px;'; 1629 + 1630 + const addBtn = document.createElement('button'); 1631 + addBtn.textContent = 'Add Profile'; 1632 + addBtn.className = 'btn-primary'; 1633 + addBtn.style.cssText = ` 1634 + padding: 8px 16px; 1635 + background: var(--accent-primary); 1636 + border: none; 1637 + border-radius: 4px; 1638 + color: white; 1639 + font-size: 13px; 1640 + cursor: pointer; 1641 + `; 1642 + addBtn.addEventListener('click', () => showAddProfileDialog(container)); 1643 + addSection.appendChild(addBtn); 1644 + 1645 + container.appendChild(addSection); 1646 + 1647 + // Profiles list section 1648 + const listSection = document.createElement('div'); 1649 + listSection.className = 'form-section'; 1650 + listSection.style.cssText = 'border-top: 1px solid var(--border-primary); padding-top: 16px;'; 1651 + 1652 + const listTitle = document.createElement('h3'); 1653 + listTitle.className = 'form-section-title'; 1654 + listTitle.textContent = 'All Profiles'; 1655 + listSection.appendChild(listTitle); 1656 + 1657 + const profilesList = document.createElement('div'); 1658 + profilesList.className = 'profiles-list'; 1659 + profilesList.style.cssText = 'display: flex; flex-direction: column; gap: 12px;'; 1660 + 1661 + for (const profile of profiles) { 1662 + const card = document.createElement('div'); 1663 + card.style.cssText = ` 1664 + padding: 12px; 1665 + background: var(--bg-secondary); 1666 + border: 1px solid var(--border-primary); 1667 + border-radius: 6px; 1668 + display: flex; 1669 + flex-direction: column; 1670 + gap: 8px; 1671 + `; 1672 + 1673 + // Profile header (radio + name + delete) 1674 + const header = document.createElement('div'); 1675 + header.style.cssText = 'display: flex; align-items: center; gap: 8px;'; 1676 + 1677 + // Radio button for switching 1678 + const radio = document.createElement('input'); 1679 + radio.type = 'radio'; 1680 + radio.name = 'profile-switch'; 1681 + radio.checked = currentProfile && profile.id === currentProfile.id; 1682 + radio.style.cursor = 'pointer'; 1683 + radio.addEventListener('change', () => { 1684 + if (radio.checked) { 1685 + if (confirm(`Switch to ${profile.name}? The app will restart.`)) { 1686 + api.profiles.switch(profile.slug).then(result => { 1687 + if (!result.success) { 1688 + alert(`Failed to switch profile: ${result.error}`); 1689 + radio.checked = false; 1690 + } 1691 + }); 1692 + } else { 1693 + radio.checked = false; 1694 + } 1695 + } 1696 + }); 1697 + header.appendChild(radio); 1698 + 1699 + // Profile name 1700 + const nameEl = document.createElement('span'); 1701 + nameEl.textContent = profile.name; 1702 + nameEl.style.cssText = 'flex: 1; font-weight: 500;'; 1703 + header.appendChild(nameEl); 1704 + 1705 + // Delete button (disabled for default or active) 1706 + const deleteBtn = document.createElement('button'); 1707 + deleteBtn.textContent = 'Delete'; 1708 + deleteBtn.disabled = profile.isDefault || (currentProfile && profile.id === currentProfile.id); 1709 + deleteBtn.style.cssText = ` 1710 + padding: 4px 12px; 1711 + background: var(--bg-tertiary); 1712 + border: 1px solid var(--border-primary); 1713 + border-radius: 4px; 1714 + color: var(--text-secondary); 1715 + font-size: 12px; 1716 + cursor: pointer; 1717 + `; 1718 + if (deleteBtn.disabled) { 1719 + deleteBtn.style.opacity = '0.5'; 1720 + deleteBtn.style.cursor = 'not-allowed'; 1721 + } 1722 + deleteBtn.addEventListener('click', async () => { 1723 + if (confirm(`Delete profile "${profile.name}"? Data will be preserved but the profile record will be removed.`)) { 1724 + const result = await api.profiles.delete(profile.id); 1725 + if (result.success) { 1726 + card.remove(); 1727 + } else { 1728 + alert(`Failed to delete profile: ${result.error}`); 1729 + } 1730 + } 1731 + }); 1732 + header.appendChild(deleteBtn); 1733 + 1734 + card.appendChild(header); 1735 + 1736 + // Sync configuration section 1737 + const syncSection = document.createElement('div'); 1738 + syncSection.style.cssText = ` 1739 + padding: 8px; 1740 + background: var(--bg-tertiary); 1741 + border-radius: 4px; 1742 + display: flex; 1743 + flex-direction: column; 1744 + gap: 6px; 1745 + `; 1746 + 1747 + const syncTitle = document.createElement('div'); 1748 + syncTitle.textContent = 'Sync Configuration'; 1749 + syncTitle.style.cssText = 'font-size: 12px; font-weight: 500; color: var(--text-secondary);'; 1750 + syncSection.appendChild(syncTitle); 1751 + 1752 + // Check if sync is enabled 1753 + let syncConfig = null; 1754 + try { 1755 + const configResult = await api.profiles.getSyncConfig(profile.id); 1756 + if (configResult.success && configResult.data) { 1757 + syncConfig = configResult.data; 1758 + } 1759 + } catch (err) { 1760 + console.error('[settings] Failed to get sync config for profile:', err); 1761 + } 1762 + 1763 + if (syncConfig) { 1764 + // Sync enabled - show details and disable button 1765 + const syncDetails = document.createElement('div'); 1766 + syncDetails.style.cssText = 'font-size: 12px; color: var(--text-secondary);'; 1767 + syncDetails.innerHTML = ` 1768 + <div>✓ Sync enabled</div> 1769 + <div>Server profile: <strong>${syncConfig.serverProfileSlug}</strong></div> 1770 + `; 1771 + syncSection.appendChild(syncDetails); 1772 + 1773 + const disableBtn = document.createElement('button'); 1774 + disableBtn.textContent = 'Disable Sync'; 1775 + disableBtn.style.cssText = ` 1776 + padding: 4px 12px; 1777 + background: var(--bg-tertiary); 1778 + border: 1px solid var(--border-primary); 1779 + border-radius: 4px; 1780 + color: var(--text-secondary); 1781 + font-size: 12px; 1782 + cursor: pointer; 1783 + align-self: flex-start; 1784 + `; 1785 + disableBtn.addEventListener('click', async () => { 1786 + if (confirm(`Disable sync for profile "${profile.name}"?`)) { 1787 + const result = await api.profiles.disableSync(profile.id); 1788 + if (result.success) { 1789 + // Refresh the profiles section 1790 + const newContent = await renderProfilesSettings(); 1791 + container.replaceWith(newContent); 1792 + } else { 1793 + alert(`Failed to disable sync: ${result.error}`); 1794 + } 1795 + } 1796 + }); 1797 + syncSection.appendChild(disableBtn); 1798 + } else { 1799 + // Sync not enabled - show enable button 1800 + const syncDisabled = document.createElement('div'); 1801 + syncDisabled.style.cssText = 'font-size: 12px; color: var(--text-secondary);'; 1802 + syncDisabled.textContent = 'Sync not configured'; 1803 + syncSection.appendChild(syncDisabled); 1804 + 1805 + const enableBtn = document.createElement('button'); 1806 + enableBtn.textContent = 'Enable Sync'; 1807 + enableBtn.style.cssText = ` 1808 + padding: 4px 12px; 1809 + background: var(--accent-primary); 1810 + border: none; 1811 + border-radius: 4px; 1812 + color: white; 1813 + font-size: 12px; 1814 + cursor: pointer; 1815 + align-self: flex-start; 1816 + `; 1817 + enableBtn.addEventListener('click', () => showEnableSyncDialog(profile, container)); 1818 + syncSection.appendChild(enableBtn); 1819 + } 1820 + 1821 + card.appendChild(syncSection); 1822 + profilesList.appendChild(card); 1823 + } 1824 + 1825 + listSection.appendChild(profilesList); 1826 + container.appendChild(listSection); 1827 + 1828 + return container; 1829 + }; 1830 + 1831 + // Show add profile dialog 1832 + const showAddProfileDialog = async (parentContainer) => { 1833 + const dialog = document.createElement('div'); 1834 + dialog.style.cssText = ` 1835 + position: fixed; 1836 + top: 0; 1837 + left: 0; 1838 + right: 0; 1839 + bottom: 0; 1840 + background: rgba(0, 0, 0, 0.5); 1841 + display: flex; 1842 + align-items: center; 1843 + justify-content: center; 1844 + z-index: 10000; 1845 + `; 1846 + 1847 + const content = document.createElement('div'); 1848 + content.style.cssText = ` 1849 + background: var(--bg-primary); 1850 + padding: 24px; 1851 + border-radius: 8px; 1852 + width: 400px; 1853 + max-width: 90%; 1854 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 1855 + `; 1856 + 1857 + const title = document.createElement('h3'); 1858 + title.textContent = 'Add Profile'; 1859 + title.style.cssText = 'margin: 0 0 16px 0;'; 1860 + content.appendChild(title); 1861 + 1862 + const input = document.createElement('input'); 1863 + input.type = 'text'; 1864 + input.placeholder = 'Profile name (e.g., Work, Personal)'; 1865 + input.style.cssText = ` 1866 + width: 100%; 1867 + padding: 8px; 1868 + border: 1px solid var(--border-primary); 1869 + border-radius: 4px; 1870 + background: var(--bg-secondary); 1871 + color: var(--text-primary); 1872 + font-size: 14px; 1873 + margin-bottom: 16px; 1874 + `; 1875 + content.appendChild(input); 1876 + 1877 + const buttons = document.createElement('div'); 1878 + buttons.style.cssText = 'display: flex; gap: 8px; justify-content: flex-end;'; 1879 + 1880 + const cancelBtn = document.createElement('button'); 1881 + cancelBtn.textContent = 'Cancel'; 1882 + cancelBtn.style.cssText = ` 1883 + padding: 8px 16px; 1884 + background: var(--bg-tertiary); 1885 + border: 1px solid var(--border-primary); 1886 + border-radius: 4px; 1887 + color: var(--text-secondary); 1888 + font-size: 13px; 1889 + cursor: pointer; 1890 + `; 1891 + cancelBtn.addEventListener('click', () => dialog.remove()); 1892 + buttons.appendChild(cancelBtn); 1893 + 1894 + const createBtn = document.createElement('button'); 1895 + createBtn.textContent = 'Create'; 1896 + createBtn.style.cssText = ` 1897 + padding: 8px 16px; 1898 + background: var(--accent-primary); 1899 + border: none; 1900 + border-radius: 4px; 1901 + color: white; 1902 + font-size: 13px; 1903 + cursor: pointer; 1904 + `; 1905 + createBtn.addEventListener('click', async () => { 1906 + const name = input.value.trim(); 1907 + if (!name) { 1908 + alert('Please enter a profile name'); 1909 + return; 1910 + } 1911 + 1912 + const result = await api.profiles.create(name); 1913 + if (result.success) { 1914 + dialog.remove(); 1915 + // Refresh the profiles section 1916 + const newContent = await renderProfilesSettings(); 1917 + parentContainer.replaceWith(newContent); 1918 + } else { 1919 + alert(`Failed to create profile: ${result.error}`); 1920 + } 1921 + }); 1922 + buttons.appendChild(createBtn); 1923 + 1924 + content.appendChild(buttons); 1925 + dialog.appendChild(content); 1926 + document.body.appendChild(dialog); 1927 + 1928 + input.focus(); 1929 + }; 1930 + 1931 + // Show enable sync dialog 1932 + const showEnableSyncDialog = async (profile, parentContainer) => { 1933 + const dialog = document.createElement('div'); 1934 + dialog.style.cssText = ` 1935 + position: fixed; 1936 + top: 0; 1937 + left: 0; 1938 + right: 0; 1939 + bottom: 0; 1940 + background: rgba(0, 0, 0, 0.5); 1941 + display: flex; 1942 + align-items: center; 1943 + justify-content: center; 1944 + z-index: 10000; 1945 + `; 1946 + 1947 + const content = document.createElement('div'); 1948 + content.style.cssText = ` 1949 + background: var(--bg-primary); 1950 + padding: 24px; 1951 + border-radius: 8px; 1952 + width: 500px; 1953 + max-width: 90%; 1954 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 1955 + `; 1956 + 1957 + const title = document.createElement('h3'); 1958 + title.textContent = `Enable Sync for "${profile.name}"`; 1959 + title.style.cssText = 'margin: 0 0 16px 0;'; 1960 + content.appendChild(title); 1961 + 1962 + const description = document.createElement('p'); 1963 + description.textContent = 'Enter your server API key and the server profile slug to sync to.'; 1964 + description.style.cssText = 'margin: 0 0 16px 0; font-size: 13px; color: var(--text-secondary);'; 1965 + content.appendChild(description); 1966 + 1967 + const apiKeyInput = document.createElement('input'); 1968 + apiKeyInput.type = 'password'; 1969 + apiKeyInput.placeholder = 'API Key'; 1970 + apiKeyInput.style.cssText = ` 1971 + width: 100%; 1972 + padding: 8px; 1973 + border: 1px solid var(--border-primary); 1974 + border-radius: 4px; 1975 + background: var(--bg-secondary); 1976 + color: var(--text-primary); 1977 + font-size: 14px; 1978 + margin-bottom: 12px; 1979 + `; 1980 + content.appendChild(apiKeyInput); 1981 + 1982 + const slugInput = document.createElement('input'); 1983 + slugInput.type = 'text'; 1984 + slugInput.placeholder = 'Server profile slug (e.g., default, work, personal)'; 1985 + slugInput.style.cssText = ` 1986 + width: 100%; 1987 + padding: 8px; 1988 + border: 1px solid var(--border-primary); 1989 + border-radius: 4px; 1990 + background: var(--bg-secondary); 1991 + color: var(--text-primary); 1992 + font-size: 14px; 1993 + margin-bottom: 16px; 1994 + `; 1995 + content.appendChild(slugInput); 1996 + 1997 + const buttons = document.createElement('div'); 1998 + buttons.style.cssText = 'display: flex; gap: 8px; justify-content: flex-end;'; 1999 + 2000 + const cancelBtn = document.createElement('button'); 2001 + cancelBtn.textContent = 'Cancel'; 2002 + cancelBtn.style.cssText = ` 2003 + padding: 8px 16px; 2004 + background: var(--bg-tertiary); 2005 + border: 1px solid var(--border-primary); 2006 + border-radius: 4px; 2007 + color: var(--text-secondary); 2008 + font-size: 13px; 2009 + cursor: pointer; 2010 + `; 2011 + cancelBtn.addEventListener('click', () => dialog.remove()); 2012 + buttons.appendChild(cancelBtn); 2013 + 2014 + const enableBtn = document.createElement('button'); 2015 + enableBtn.textContent = 'Enable'; 2016 + enableBtn.style.cssText = ` 2017 + padding: 8px 16px; 2018 + background: var(--accent-primary); 2019 + border: none; 2020 + border-radius: 4px; 2021 + color: white; 2022 + font-size: 13px; 2023 + cursor: pointer; 2024 + `; 2025 + enableBtn.addEventListener('click', async () => { 2026 + const apiKey = apiKeyInput.value.trim(); 2027 + const serverProfileSlug = slugInput.value.trim(); 2028 + 2029 + if (!apiKey) { 2030 + alert('Please enter an API key'); 2031 + return; 2032 + } 2033 + 2034 + if (!serverProfileSlug) { 2035 + alert('Please enter a server profile slug'); 2036 + return; 2037 + } 2038 + 2039 + const result = await api.profiles.enableSync(profile.id, apiKey, serverProfileSlug); 2040 + if (result.success) { 2041 + dialog.remove(); 2042 + // Refresh the profiles section 2043 + const newContent = await renderProfilesSettings(); 2044 + parentContainer.replaceWith(newContent); 2045 + } else { 2046 + alert(`Failed to enable sync: ${result.error}`); 2047 + } 2048 + }); 2049 + buttons.appendChild(enableBtn); 2050 + 2051 + content.appendChild(buttons); 2052 + dialog.appendChild(content); 2053 + document.body.appendChild(dialog); 2054 + 2055 + apiKeyInput.focus(); 2056 + }; 2057 + 1587 2058 // Initialize 1588 2059 const init = async () => { 1589 2060 const sidebarNav = document.getElementById('sidebarNav'); ··· 1774 2245 }); 1775 2246 1776 2247 contentArea.appendChild(syncSection); 2248 + 2249 + // Add Profiles management section 2250 + const profilesNav = document.createElement('a'); 2251 + profilesNav.className = 'nav-item'; 2252 + profilesNav.textContent = 'Profiles'; 2253 + profilesNav.dataset.section = 'profiles'; 2254 + profilesNav.addEventListener('click', () => showSection('profiles')); 2255 + sidebarNav.appendChild(profilesNav); 2256 + 2257 + // Create profiles section with async content 2258 + const profilesSection = document.createElement('div'); 2259 + profilesSection.className = 'section'; 2260 + profilesSection.id = 'section-profiles'; 2261 + 2262 + const profilesTitle = document.createElement('h2'); 2263 + profilesTitle.className = 'section-title'; 2264 + profilesTitle.textContent = 'Profiles'; 2265 + profilesSection.appendChild(profilesTitle); 2266 + 2267 + // Load profiles content async 2268 + renderProfilesSettings().then(content => { 2269 + profilesSection.appendChild(content); 2270 + }); 2271 + 2272 + contentArea.appendChild(profilesSection); 1777 2273 1778 2274 // Add Themes management section 1779 2275 const themesNav = document.createElement('a');