Skip to content

Commit 40a189f

Browse files
committed
Implement tab functionality and add help documentation
Add JavaScript for tab navigation, new feed management features, and comprehensive help section. Includes setup wizard, data management tools, and improved user guidance. Remove unused package script.
1 parent c289de2 commit 40a189f

18 files changed

Lines changed: 631 additions & 216 deletions

options/options.js

Lines changed: 226 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,61 @@ if (typeof document !== 'undefined') {
4646

4747
// Theme listener imported from controllers/theme-controller.js
4848

49+
function setupTabNavigation() {
50+
const tabButtons = document.querySelectorAll('.tab-button');
51+
const tabPanels = document.querySelectorAll('.tab-panel');
52+
53+
// Function to switch tabs
54+
function switchTab(tabName) {
55+
// Update buttons
56+
tabButtons.forEach(btn => {
57+
const isActive = btn.dataset.tab === tabName;
58+
btn.classList.toggle('active', isActive);
59+
btn.setAttribute('aria-selected', isActive);
60+
});
61+
62+
// Update panels
63+
tabPanels.forEach(panel => {
64+
panel.classList.toggle('active', panel.dataset.tab === tabName);
65+
});
66+
67+
// Save to localStorage
68+
localStorage.setItem('activeTab', tabName);
69+
70+
// Update URL hash without scrolling
71+
history.replaceState(null, null, `#${tabName}`);
72+
}
73+
74+
// Add click listeners to tab buttons
75+
tabButtons.forEach(button => {
76+
button.addEventListener('click', () => {
77+
switchTab(button.dataset.tab);
78+
});
79+
});
80+
81+
// Add click listeners to clickable setup steps
82+
const clickableSetupSteps = document.querySelectorAll('.setup-step.clickable');
83+
clickableSetupSteps.forEach(step => {
84+
step.addEventListener('click', () => {
85+
const tabName = step.dataset.tab;
86+
if (tabName) {
87+
switchTab(tabName);
88+
}
89+
});
90+
});
91+
92+
// Initialize active tab from localStorage or URL hash
93+
const hash = window.location.hash.substring(1);
94+
const savedTab = localStorage.getItem('activeTab');
95+
const initialTab = hash || savedTab || 'setup';
96+
97+
// Check if the hash/saved tab is valid
98+
const validTabs = ['setup', 'repositories', 'filters', 'preferences', 'advanced', 'help'];
99+
const tabToActivate = validTabs.includes(initialTab) ? initialTab : 'setup';
100+
101+
switchTab(tabToActivate);
102+
}
103+
49104
function handleUrlParameters() {
50105
// Check for URL parameters to enhance user experience
51106
const urlParams = new URLSearchParams(window.location.search);
@@ -62,21 +117,48 @@ function handleUrlParameters() {
62117
}, 100);
63118
}
64119

65-
// Handle hash navigation to scroll to specific section
120+
// Map old section hashes to tabs for backwards compatibility
121+
const sectionToTabMap = {
122+
'token': 'setup',
123+
'repositories': 'repositories',
124+
'filters': 'filters',
125+
'feed-management': 'preferences',
126+
'appearance': 'preferences',
127+
'interval': 'preferences',
128+
'snooze': 'preferences',
129+
'import-export': 'advanced'
130+
};
131+
132+
// Handle hash navigation - if it's an old section hash, switch to appropriate tab
66133
if (hash) {
67-
setTimeout(() => {
68-
// Extract just the hash part without query parameters
69-
const hashParts = hash.split('?');
70-
const cleanHash = hashParts[0];
71-
const targetSection = document.querySelector(cleanHash);
72-
if (targetSection) {
73-
targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
74-
}
75-
}, 200);
134+
const cleanHash = hash.substring(1).split('?')[0];
135+
const targetTab = sectionToTabMap[cleanHash];
136+
137+
if (targetTab) {
138+
// Switch to the tab containing this section
139+
setTimeout(() => {
140+
const tabButtons = document.querySelectorAll('.tab-button');
141+
const targetButton = Array.from(tabButtons).find(btn => btn.dataset.tab === targetTab);
142+
if (targetButton) {
143+
targetButton.click();
144+
145+
// Then scroll to the section within that tab
146+
setTimeout(() => {
147+
const targetSection = document.getElementById(cleanHash);
148+
if (targetSection) {
149+
targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
150+
}
151+
}, 100);
152+
}
153+
}, 150);
154+
}
76155
}
77156
}
78157

79158
function setupEventListeners() {
159+
// Tab navigation
160+
setupTabNavigation();
161+
80162
document.getElementById('addRepoBtn').addEventListener('click', addRepo);
81163
document.getElementById('clearTokenBtn').addEventListener('click', clearToken);
82164

@@ -234,6 +316,39 @@ function setupEventListeners() {
234316
});
235317
});
236318

319+
// Auto-save itemExpiryHours changes
320+
const itemExpiryEnabledCheckbox = document.getElementById('itemExpiryEnabled');
321+
const itemExpiryHoursInput = document.getElementById('itemExpiryHours');
322+
const itemExpiryInputRow = document.getElementById('itemExpiryInputRow');
323+
324+
itemExpiryEnabledCheckbox.addEventListener('change', async (e) => {
325+
const isEnabled = e.target.checked;
326+
if (isEnabled) {
327+
itemExpiryInputRow.style.display = 'block';
328+
const hours = parseInt(itemExpiryHoursInput.value) || 24;
329+
itemExpiryHoursInput.value = hours;
330+
await chrome.storage.sync.set({ itemExpiryHours: hours });
331+
toastManager.info(`Auto-removal enabled: items older than ${hours} hours will be removed`);
332+
} else {
333+
itemExpiryInputRow.style.display = 'none';
334+
await chrome.storage.sync.set({ itemExpiryHours: null });
335+
toastManager.info('Auto-removal disabled');
336+
}
337+
});
338+
339+
itemExpiryHoursInput.addEventListener('change', async (e) => {
340+
let hours = parseInt(e.target.value);
341+
if (isNaN(hours) || hours < 1) {
342+
hours = 1;
343+
e.target.value = 1;
344+
} else if (hours > 168) {
345+
hours = 168;
346+
e.target.value = 168;
347+
}
348+
await chrome.storage.sync.set({ itemExpiryHours: hours });
349+
toastManager.info(`Auto-removal time changed to ${hours} hours`);
350+
});
351+
237352
// Auto-save filter and notification changes
238353
['filterPrs', 'filterIssues', 'filterReleases', 'notifyPrs', 'notifyIssues', 'notifyReleases'].forEach(id => {
239354
document.getElementById(id).addEventListener('change', async (e) => {
@@ -327,6 +442,11 @@ function setupEventListeners() {
327442
}
328443
});
329444

445+
// Data Management buttons
446+
document.getElementById('clearCacheBtn').addEventListener('click', clearCacheData);
447+
document.getElementById('clearAllDataBtn').addEventListener('click', clearAllData);
448+
document.getElementById('resetSettingsBtn').addEventListener('click', resetSettings);
449+
330450
}
331451

332452
// Panel toggle functionality
@@ -448,7 +568,8 @@ async function loadSettings() {
448568
'snoozeHours',
449569
'filters',
450570
'notifications',
451-
'theme'
571+
'theme',
572+
'itemExpiryHours'
452573
]);
453574

454575
const snoozeSettings = await chrome.storage.sync.get(['snoozedRepos']);
@@ -522,6 +643,14 @@ async function loadSettings() {
522643
themeRadio.checked = true;
523644
}
524645

646+
// Load itemExpiryHours setting
647+
const itemExpiryEnabled = settings.itemExpiryHours !== null && settings.itemExpiryHours !== undefined;
648+
document.getElementById('itemExpiryEnabled').checked = itemExpiryEnabled;
649+
if (itemExpiryEnabled) {
650+
document.getElementById('itemExpiryHours').value = settings.itemExpiryHours;
651+
document.getElementById('itemExpiryInputRow').style.display = 'block';
652+
}
653+
525654
// Update notification toggle states after loading settings
526655
updateNotificationToggleStates();
527656

@@ -874,12 +1003,93 @@ if (typeof module !== 'undefined' && module.exports) {
8741003
formatNumber,
8751004
formatDate: formatDateVerbose, // Export verbose formatter for tests
8761005
exportSettings,
877-
handleImportFile
1006+
handleImportFile,
1007+
clearCacheData,
1008+
clearAllData,
1009+
resetSettings
8781010
};
8791011
}
8801012

8811013
// Snooze functions are now imported from controllers/snooze-controller.js
8821014

1015+
// Data Management Functions
1016+
/**
1017+
* Clear cache data (activities only, preserves settings and repos)
1018+
*/
1019+
async function clearCacheData() {
1020+
try {
1021+
await setLocalItem('activities', []);
1022+
await setLocalItem('readItems', []);
1023+
1024+
// Reset badge count
1025+
chrome.runtime.sendMessage({ action: 'clearBadge' });
1026+
1027+
toastManager.success('Cache cleared successfully');
1028+
} catch (error) {
1029+
console.error('Error clearing cache:', error);
1030+
toastManager.error('Failed to clear cache');
1031+
}
1032+
}
1033+
1034+
/**
1035+
* Clear all data (activities and activity history, preserves settings and repos)
1036+
*/
1037+
async function clearAllData() {
1038+
// Show confirmation dialog
1039+
const confirmed = confirm(
1040+
'This will clear all cached data and activity history, but preserve your settings and repositories.\n\nAre you sure you want to continue?'
1041+
);
1042+
1043+
if (!confirmed) {
1044+
return;
1045+
}
1046+
1047+
try {
1048+
// Clear local storage (activities, readItems, collapsedRepos, etc.)
1049+
await chrome.storage.local.clear();
1050+
1051+
// Reset badge count
1052+
chrome.runtime.sendMessage({ action: 'clearBadge' });
1053+
1054+
toastManager.success('All data cleared successfully');
1055+
} catch (error) {
1056+
console.error('Error clearing all data:', error);
1057+
toastManager.error('Failed to clear data');
1058+
}
1059+
}
1060+
1061+
/**
1062+
* Reset all settings to defaults (clears everything)
1063+
*/
1064+
async function resetSettings() {
1065+
// Show confirmation dialog
1066+
const confirmed = confirm(
1067+
'This will reset ALL settings to defaults and clear your GitHub token and repositories.\n\nThis action cannot be undone. Are you sure?'
1068+
);
1069+
1070+
if (!confirmed) {
1071+
return;
1072+
}
1073+
1074+
try {
1075+
// Clear all storage (both sync and local)
1076+
await chrome.storage.sync.clear();
1077+
await chrome.storage.local.clear();
1078+
1079+
// Reset badge count
1080+
chrome.runtime.sendMessage({ action: 'clearBadge' });
1081+
1082+
toastManager.success('Settings reset to defaults');
1083+
1084+
// Reload the page to show default state
1085+
setTimeout(() => {
1086+
window.location.reload();
1087+
}, 1000);
1088+
} catch (error) {
1089+
console.error('Error resetting settings:', error);
1090+
toastManager.error('Failed to reset settings');
1091+
}
1092+
}
8831093

8841094
// Auto-refresh snoozed repos list every 5 minutes
8851095
setInterval(async () => {
@@ -900,5 +1110,8 @@ export {
9001110
formatNumber,
9011111
formatDateVerbose as formatDate, // Export verbose formatter for tests
9021112
exportSettings,
903-
handleImportFile
1113+
handleImportFile,
1114+
clearCacheData,
1115+
clearAllData,
1116+
resetSettings
9041117
};

popup/controllers/activity-controller.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@ import { setState } from '../../shared/state-manager.js';
1515
* @param {Function} renderActivitiesCallback - Callback to render activities after loading
1616
* @param {Function} setPinnedReposCallback - Callback to update pinned repos
1717
* @param {Function} setCollapsedReposCallback - Callback to update collapsed repos
18+
* @param {Object} options - Loading options
19+
* @param {boolean} options.skipLoadingIndicator - Don't show loading state if true (for refresh)
1820
*/
1921
export async function loadActivities(
2022
renderActivitiesCallback,
2123
setPinnedReposCallback,
22-
setCollapsedReposCallback
24+
setCollapsedReposCallback,
25+
options = {}
2326
) {
27+
const { skipLoadingIndicator = false } = options;
2428
const list = document.getElementById('activityList');
2529

2630
// Check offline status first
2731
if (isOffline()) {
28-
list.innerHTML = '<div class="loading" role="status" aria-live="polite">Loading cached data...</div>';
32+
if (!skipLoadingIndicator) {
33+
list.innerHTML = '<div class="loading" role="status" aria-live="polite">Loading cached data...</div>';
34+
}
2935
showOfflineStatus('errorMessage', true);
3036

3137
try {
@@ -49,26 +55,32 @@ export async function loadActivities(
4955
renderActivitiesCallback();
5056
return;
5157
} else {
52-
list.innerHTML = `
53-
<div class="empty-state">
54-
<div class="offline-empty">
55-
<div class="offline-icon">📡</div>
56-
<p>No cached data available</p>
57-
<small>Check your connection and try again</small>
58+
if (!skipLoadingIndicator) {
59+
list.innerHTML = `
60+
<div class="empty-state">
61+
<div class="offline-empty">
62+
<div class="offline-icon">📡</div>
63+
<p>No cached data available</p>
64+
<small>Check your connection and try again</small>
65+
</div>
5866
</div>
59-
</div>
60-
`;
67+
`;
68+
}
6169
return;
6270
}
6371
} catch (error) {
64-
list.innerHTML = '<div class="empty-state"><p>Unable to load cached data</p></div>';
72+
if (!skipLoadingIndicator) {
73+
list.innerHTML = '<div class="empty-state"><p>Unable to load cached data</p></div>';
74+
}
6575
showError('errorMessage', error, null, { action: 'load cached activities' }, 0);
6676
return;
6777
}
6878
}
6979

7080
// Online mode - proceed normally
71-
list.innerHTML = '<div class="loading" role="status" aria-live="polite">Loading...</div>';
81+
if (!skipLoadingIndicator) {
82+
list.innerHTML = '<div class="loading" role="status" aria-live="polite">Loading...</div>';
83+
}
7284
showOfflineStatus('errorMessage', false);
7385
clearError('errorMessage');
7486

@@ -104,7 +116,9 @@ export async function loadActivities(
104116
showStoredError(data.lastError);
105117
}
106118
} catch (error) {
107-
list.innerHTML = '<div class="empty-state"><p>Unable to load activities</p></div>';
119+
if (!skipLoadingIndicator) {
120+
list.innerHTML = '<div class="empty-state"><p>Unable to load activities</p></div>';
121+
}
108122
showError('errorMessage', error, null, { action: 'load activities' }, 0);
109123
}
110124
}
@@ -121,7 +135,8 @@ export async function handleRefresh(loadActivitiesCallback) {
121135
try {
122136
clearError('errorMessage');
123137
await chrome.runtime.sendMessage({ action: 'checkNow' });
124-
await loadActivitiesCallback();
138+
// Pass skipLoadingIndicator to avoid clearing the existing feed
139+
await loadActivitiesCallback(null, null, null, { skipLoadingIndicator: true });
125140
} catch (error) {
126141
showError('errorMessage', error, null, { action: 'refresh activities' }, 5000);
127142
} finally {

0 commit comments

Comments
 (0)