diff --git a/src/background/controller/overall.ts b/src/background/controller/overall.ts index 9b75dd5..801ae9f 100644 --- a/src/background/controller/overall.ts +++ b/src/background/controller/overall.ts @@ -8,7 +8,10 @@ export async function updateTotalTime( const timeline = await getActivityTimeline(currentIsoDate); const timeOnRecord = timeline .filter((t) => t.hostname === hostname) - .reduce((acc, t) => acc + t.activityPeriodEnd - t.activityPeriodStart, 0); + .reduce((acc, t) => { + const duration = t.activityPeriodEnd - t.activityPeriodStart + return acc + Math.abs(duration) + }, 0); await setTotalDailyHostTime({ date: currentIsoDate, diff --git a/src/popup/components/ActivityDatePicker/index.tsx b/src/popup/components/ActivityDatePicker/index.tsx index 9b50ddc..8e82d92 100644 --- a/src/popup/components/ActivityDatePicker/index.tsx +++ b/src/popup/components/ActivityDatePicker/index.tsx @@ -38,6 +38,7 @@ export const ActivityDatePicker: React.FC = ({ @@ -50,6 +51,7 @@ export const ActivityDatePicker: React.FC = ({ diff --git a/src/popup/components/ActivityPageDailyActivityTab/ActivityPageDailyActivityTab.tsx b/src/popup/components/ActivityPageDailyActivityTab/ActivityPageDailyActivityTab.tsx index 2886de9..5f54150 100644 --- a/src/popup/components/ActivityPageDailyActivityTab/ActivityPageDailyActivityTab.tsx +++ b/src/popup/components/ActivityPageDailyActivityTab/ActivityPageDailyActivityTab.tsx @@ -67,6 +67,7 @@ export const DailyActivityTab: React.FC = ({ title="Activity Timeline" activityTimeline={activityTimeline} filteredHostname={filteredHostname} + description="Your web activity timeline." /> = + ({ store, sundayDate }) => { + const [pickedDomain, setPickedDomain] = React.useState(null); + const scrollToRef = React.useRef(null); + + const handleDomainRowClick = React.useCallback((domain: string) => { + setPickedDomain(domain); + scrollToRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + const allMonthlyActivity = React.useMemo( + () => + get30DaysPriorDate(sundayDate).reduce((acc, date) => { + const isoDate = getIsoDate(date); + acc[isoDate] = store[isoDate] || {}; + + return acc; + }, {} as TimeStore), + [store, sundayDate] + ); + + const filteredWebsiteMonthActivity = React.useMemo(() => { + if (pickedDomain === null) { + return allMonthlyActivity; + } + + return Object.entries(allMonthlyActivity).reduce( + (acc, [date, dateWebsitesUsage]) => { + acc[date] = { + [pickedDomain]: dateWebsitesUsage[pickedDomain] || 0, + }; + + return acc; + }, + {} as typeof allMonthlyActivity + ); + }, [allMonthlyActivity, pickedDomain]); + + const totalWebsiteMonthlyActivity = React.useMemo( + () => + Object.values(allMonthlyActivity).reduce((acc, dailyUsage) => { + Object.entries(dailyUsage).forEach(([key, value]) => { + acc[key] ??= 0; + acc[key] += value; + }); + + return acc; + }, {} as Record), + [allMonthlyActivity] + ); + + const averageMonthlyActivity = React.useMemo(() => { + const averageMonthly = + getTotalMonthlyActivity(filteredWebsiteMonthActivity, sundayDate) / 7; + return averageMonthly; + }, [filteredWebsiteMonthActivity, sundayDate]); + + const presentedPickedDomain = pickedDomain ?? 'All Websites'; + + return ( +
+ +
+ + `Activity on ${presentedPickedDomain} per day` + } + /> +
+ +
+ ); + }; diff --git a/src/popup/components/ActivityPageMonthlyActivityTab/types.ts b/src/popup/components/ActivityPageMonthlyActivityTab/types.ts new file mode 100644 index 0000000..0fb681e --- /dev/null +++ b/src/popup/components/ActivityPageMonthlyActivityTab/types.ts @@ -0,0 +1,6 @@ +import { TimeStore } from '../../hooks/useTimeStore'; + +export interface ActivityPageMonthlyActivityTabProps { + store: TimeStore; + sundayDate: Date; +} diff --git a/src/popup/components/ActivityPageWeeklyActivityTab/ActivityPageWeeklyActivityTab.tsx b/src/popup/components/ActivityPageWeeklyActivityTab/ActivityPageWeeklyActivityTab.tsx index de4922f..cf46b69 100644 --- a/src/popup/components/ActivityPageWeeklyActivityTab/ActivityPageWeeklyActivityTab.tsx +++ b/src/popup/components/ActivityPageWeeklyActivityTab/ActivityPageWeeklyActivityTab.tsx @@ -48,6 +48,7 @@ export const ActivityPageWeeklyActivityTab: React.FC Object.values(allWeekActivity).reduce((acc, dailyUsage) => { @@ -61,6 +62,7 @@ export const ActivityPageWeeklyActivityTab: React.FC { const averageWeekly = getTotalWeeklyActivity(filteredWebsiteWeekActivity, sundayDate) / 7; diff --git a/src/popup/components/GeneralTimeline/GeneralTimeline.tsx b/src/popup/components/GeneralTimeline/GeneralTimeline.tsx index c4e0039..0863e71 100644 --- a/src/popup/components/GeneralTimeline/GeneralTimeline.tsx +++ b/src/popup/components/GeneralTimeline/GeneralTimeline.tsx @@ -13,6 +13,7 @@ const GeneralTimelineFC: React.FC = ({ activityTimeline, title, emptyHoursMarginCount = 2, + description, }) => { return ( @@ -20,6 +21,7 @@ const GeneralTimelineFC: React.FC = ({ {title} {filteredHostname ? ` On ${filteredHostname}` : ''} + ({description}) = ({
{/* @ts-expect-error -- expected, this element does have props */} +
+ Less +
+
+
+
+ More +
{ settings.ignoredHosts ); const [domainToIgnore, setDomainToIgnore] = React.useState(''); - const [isDomainsListExpanded, setDomainsListExpanded] = - React.useState(false); + const [state, setState] = React.useState<{ + status: boolean, + statusText: string, + }>({ + status: false, + statusText: '', + }); const handleAddIgnoredDomain = React.useCallback(() => { try { - assertDomainIsValid(domainToIgnore); - setIgnoredDomains((prev) => { - const newIgnoredHostList = Array.from( - new Set([...prev, domainToIgnore]) - ); - - updateSettings({ - ignoredHosts: newIgnoredHostList, + const ignoredHostsList = domainToIgnore.split(',') + for (const host of ignoredHostsList) { + assertDomainIsValid(host.trim()); + setIgnoredDomains((prev) => { + const newIgnoredHostList = Array.from( + new Set([...prev, host.trim()]) + ); + + updateSettings({ + ignoredHosts: newIgnoredHostList, + }); + + return newIgnoredHostList; }); + } - return newIgnoredHostList; - }); - + setState((prev) => ({ + ...prev, + status: false, + statusText: '' + })); setDomainToIgnore(''); - } catch (_) { - // + } catch (error) { + const errorMessage = (error as Error)?.message; + + setState((prev) => ({ + ...prev, + status: true, + statusText: errorMessage + })); } }, [domainToIgnore, updateSettings]); @@ -54,16 +72,20 @@ export const IgnoredDomainSetting: React.FC = () => { ); const handleDomainToIgnoreChange = React.useCallback( - (e: React.ChangeEvent) => { + (e: React.ChangeEvent) => { setDomainToIgnore(e.target.value); }, [] ); - const handleToggleDomainsListExpanded = React.useCallback(() => { - setDomainsListExpanded((prev) => !prev); - }, [setDomainsListExpanded]); + const handleKeyDown = ((event: React.KeyboardEvent) => { + if(event.key === 'Enter') { + event.preventDefault(); + handleAddIgnoredDomain() + } + }) + const { status, statusText } = state; return (
@@ -71,10 +93,11 @@ export const IgnoredDomainSetting: React.FC = () => {
+
+ {status + && (

{statusText}

) + } +
- - View all blacklisted domains - -
+
{!ignoredDomains.length && (

No blacklisted domains

)} @@ -101,7 +122,7 @@ export const IgnoredDomainSetting: React.FC = () => {
handleRemoveIgnoredDomain(domain)} /> {domain} diff --git a/src/popup/components/LimitsSetting/LimitsSetting.tsx b/src/popup/components/LimitsSetting/LimitsSetting.tsx index b3ba1cb..946b27c 100644 --- a/src/popup/components/LimitsSetting/LimitsSetting.tsx +++ b/src/popup/components/LimitsSetting/LimitsSetting.tsx @@ -114,7 +114,7 @@ export const LimitsSetting: React.FC = () => { > handleLimitRemove(domain)} /> {domain} diff --git a/src/popup/components/MonthDatePicker/MonthDatePicker.tsx b/src/popup/components/MonthDatePicker/MonthDatePicker.tsx new file mode 100644 index 0000000..dd35810 --- /dev/null +++ b/src/popup/components/MonthDatePicker/MonthDatePicker.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import { Button, ButtonType } from '../../../blocks/Button'; +import { Icon, IconType } from '../../../blocks/Icon'; +import { getIsoDate } from '../../../shared/utils/dates-helper'; + +import { MonthDatePickerProps } from './types'; + +export const MonthDatePicker: React.FC = ({ + onMonthChange, + sundayDate, +}) => { + const monthStartDate = new Date(); + monthStartDate.setDate(sundayDate.getDate() - 30); + + const handleChangeWeekButtonClick = React.useCallback( + (direction) => { + const newWeekEndDate = new Date(sundayDate); + newWeekEndDate.setDate(sundayDate.getDate() + direction * 30); + + onMonthChange(newWeekEndDate); + }, + [sundayDate, onMonthChange] + ); + + return ( +
+ +
+ {getIsoDate(monthStartDate)} +
+ {getIsoDate(sundayDate)} +
+ +
+ ); +}; diff --git a/src/popup/components/MonthDatePicker/types.ts b/src/popup/components/MonthDatePicker/types.ts new file mode 100644 index 0000000..f095de5 --- /dev/null +++ b/src/popup/components/MonthDatePicker/types.ts @@ -0,0 +1,4 @@ +export interface MonthDatePickerProps { + sundayDate: Date; + onMonthChange: (weekEndDate: Date) => void; +} diff --git a/src/popup/components/MonthlyWebsiteActivityChart/MonthlyWebsiteActivityChart.tsx b/src/popup/components/MonthlyWebsiteActivityChart/MonthlyWebsiteActivityChart.tsx new file mode 100644 index 0000000..0763388 --- /dev/null +++ b/src/popup/components/MonthlyWebsiteActivityChart/MonthlyWebsiteActivityChart.tsx @@ -0,0 +1,130 @@ +import { Icon, IconType } from '../../../blocks/Icon'; +import { Panel, PanelHeader } from '../../../blocks/Panel'; +import { + get30DaysPriorDate, + getHoursInMs, + getIsoDate, + getTimeFromMs, + getTimeWithoutSeconds, +} from '../../../shared/utils/dates-helper'; +import { useIsDarkMode } from '../../hooks/useTheme'; +import { getTotalDailyActivity } from '../../selectors/get-total-daily-activity'; +import { BarItemTitleType, BarItemType } from '../WeeklyWebsiteActivityChart/types'; +import { MonthlyWebsiteActivityChartProps } from './types'; +import * as React from 'react'; +import { Bar } from 'react-chartjs-2'; + +const HOUR_IN_MS = getHoursInMs(1); + +const BAR_OPTIONS = { + plugins: { + legend: { + display: false, + }, + tooltip: { + callbacks: { + label: (item: BarItemType) => { + return ( + ' ' + getTimeFromMs(Number(item.formattedValue || 0) * HOUR_IN_MS) + ); + }, + title: ([item]: BarItemTitleType[]) => { + return `${item?.label}`; + }, + }, + }, + }, + responsive: true, + scales: { + x: { + ticks: { + color: '#222', + }, + }, + y: { + ticks: { + callback: (value: number) => { + return getTimeWithoutSeconds(value * HOUR_IN_MS); + }, + color: '#222', + }, + }, + }, +}; + +const DARK_MODE_BAR_OPTIONS = { + ...BAR_OPTIONS, + scales: { + ...BAR_OPTIONS.scales, + x: { + ...BAR_OPTIONS.scales.x, + grid: { + color: '#444444', + }, + ticks: { + ...BAR_OPTIONS.scales.x.ticks, + color: '#e5e5e5', + }, + }, + y: { + ...BAR_OPTIONS.scales.y, + grid: { + color: '#444444', + }, + ticks: { + ...BAR_OPTIONS.scales.y.ticks, + color: '#e5e5e5', + }, + }, + }, +}; + +export const MonthlyWebsiteActivityChart: React.FC< + MonthlyWebsiteActivityChartProps +> = ({ store, sundayDate, presentChartTitle }) => { + const isDarkMode = useIsDarkMode(); + + const [labels, data] = React.useMemo(() => { + const month = get30DaysPriorDate(sundayDate).reverse(); + const labels = month.map((date) => getIsoDate(date)); + const data = month.map( + (date) => getTotalDailyActivity(store, date) / HOUR_IN_MS, + ); + + return [labels, data]; + }, [store, sundayDate]); + + const monthName = React.useMemo( + () => `${labels[0]} - ${labels[labels.length - 1]}`, + [labels], + ); + + const chartData = React.useMemo( + () => ({ + datasets: [ + { + backgroundColor: '#4b76e3', + borderRadius: 12, + borderSkipped: false, + data: data, + label: 'Monthly activity', + }, + ], + labels: labels, + }), + [labels, data], + ); + + return ( + + + + {presentChartTitle?.(monthName) ?? monthName} + + + + ); +}; diff --git a/src/popup/components/MonthlyWebsiteActivityChart/types.ts b/src/popup/components/MonthlyWebsiteActivityChart/types.ts new file mode 100644 index 0000000..9d430a9 --- /dev/null +++ b/src/popup/components/MonthlyWebsiteActivityChart/types.ts @@ -0,0 +1,7 @@ +import { TimeStore } from '../../hooks/useTimeStore'; + +export interface MonthlyWebsiteActivityChartProps { + store: TimeStore; + sundayDate: Date; + presentChartTitle?: (weekName: string) => string; +} diff --git a/src/popup/components/OverallActivityCalendar/OverallActivtyCalendar.tsx b/src/popup/components/OverallActivityCalendar/OverallActivtyCalendar.tsx index 0a58f79..03a07c8 100644 --- a/src/popup/components/OverallActivityCalendar/OverallActivtyCalendar.tsx +++ b/src/popup/components/OverallActivityCalendar/OverallActivtyCalendar.tsx @@ -34,7 +34,7 @@ export const OverallActivityCalendarPanel: React.FC - Overall Activity + Overall Activity (Your overall activity timeline.) = ({ > handleHideDomainClick(domain)} /> = ({ {domain} - {getTimeFromMs(time)} + {getTimeFromMs(Math.abs(time))}
); diff --git a/src/popup/components/WeekDatePicker/WeekDatePicker.tsx b/src/popup/components/WeekDatePicker/WeekDatePicker.tsx index 285d435..3954b13 100644 --- a/src/popup/components/WeekDatePicker/WeekDatePicker.tsx +++ b/src/popup/components/WeekDatePicker/WeekDatePicker.tsx @@ -28,6 +28,7 @@ export const WeekDatePicker: React.FC = ({ @@ -39,6 +40,7 @@ export const WeekDatePicker: React.FC = ({ diff --git a/src/popup/components/WeeklyWebsiteActivityChart/types.ts b/src/popup/components/WeeklyWebsiteActivityChart/types.ts index 0dde6a3..72928a9 100644 --- a/src/popup/components/WeeklyWebsiteActivityChart/types.ts +++ b/src/popup/components/WeeklyWebsiteActivityChart/types.ts @@ -5,3 +5,11 @@ export interface WeeklyWebsiteActivityChartProps { sundayDate: Date; presentChartTitle?: (weekName: string) => string; } + +export interface BarItemType { + formattedValue: string | number; +} + +export interface BarItemTitleType { + label: number |string | undefined; +} \ No newline at end of file diff --git a/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx b/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx index 4892106..32027bc 100644 --- a/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx +++ b/src/popup/components/WhitelistDomainsSetting/WhitelistDomainSetting.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { twMerge } from 'tailwind-merge'; +// import { twMerge } from 'tailwind-merge'; import { Button, ButtonType } from '../../../blocks/Button'; import { Icon, IconType } from '../../../blocks/Icon'; -import { Input } from '../../../blocks/Input'; +import { TextArea } from '../../../blocks/Input'; import { PanelBody } from '../../../blocks/Panel'; import { assertDomainIsValid } from '../../../shared/utils/domains'; import { usePopupContext } from '../../hooks/PopupContext'; @@ -14,27 +14,46 @@ export const WhitelistDomainSetting: React.FC = () => { settings.allowedHosts ?? [] ); const [ newAllowedHost, setNewAllowedHost] = React.useState(''); - const [isAllowedHostsListExpanded, setAllowedHostListExpanded] = - React.useState(false); + const [state, setState] = React.useState<{ + status: boolean, + statusText: string, + }>({ + status: false, + statusText: '', + }); const handleAddWhitelistDomain = React.useCallback(() => { try { - assertDomainIsValid(newAllowedHost); - setAllowedHosts((prev) => { - const newAllowedHostList = Array.from( - new Set([...prev, newAllowedHost]) - ); - - updateSettings({ - allowedHosts: newAllowedHostList, + const allowedHostsList = newAllowedHost.split(',') + for (const host of allowedHostsList) { + assertDomainIsValid(host.trim()); + setAllowedHosts((prev) => { + const newAllowedHostList = Array.from( + new Set([...prev, host.trim()]) + ); + + updateSettings({ + allowedHosts: newAllowedHostList, + }); + + return newAllowedHostList; }); - - return newAllowedHostList; - }); + } setNewAllowedHost(''); - } catch (_) { - // + setState((prev) => ({ + ...prev, + status: false, + statusText: '' + })); + } catch (error) { + const errorMessage = (error as Error)?.message; + + setState((prev) => ({ + ...prev, + status: true, + statusText: errorMessage + })); } }, [newAllowedHost, updateSettings]); @@ -54,16 +73,20 @@ export const WhitelistDomainSetting: React.FC = () => { ); const handleAddtoAllowedHostChange = React.useCallback( - (e: React.ChangeEvent) => { + (e: React.ChangeEvent) => { setNewAllowedHost(e.target.value); }, [] ); - const handleAllowHostsListExpanded = React.useCallback(() => { - setAllowedHostListExpanded((prev) => !prev); - }, [setAllowedHostListExpanded]); + const handleKeyDown = (event: React.KeyboardEvent) => { + if(event.key === 'Enter') { + event.preventDefault(); + handleAddWhitelistDomain() + } + } + const { status, statusText } = state; return (
@@ -71,10 +94,11 @@ export const WhitelistDomainSetting: React.FC = () => {
+
+ {status + && (

{statusText}

) + } +
- - View all whitelisted domains - -
+
{!allowedHosts.length && (

No whitelisted domains

)} @@ -101,7 +123,7 @@ export const WhitelistDomainSetting: React.FC = () => {
handleRemoveAllowedHost(domain)} /> {domain} diff --git a/src/popup/pages/ActivityPage.tsx b/src/popup/pages/ActivityPage.tsx index 0df9882..fbd2bec 100644 --- a/src/popup/pages/ActivityPage.tsx +++ b/src/popup/pages/ActivityPage.tsx @@ -12,6 +12,8 @@ import { DailyActivityTab } from '../components/ActivityPageDailyActivityTab/Act import { ActivityPageWeeklyActivityTab } from '../components/ActivityPageWeeklyActivityTab/ActivityPageWeeklyActivityTab'; import { WeekDatePicker } from '../components/WeekDatePicker/WeekDatePicker'; import { usePopupContext } from '../hooks/PopupContext'; +import { MonthDatePicker } from '../components/MonthDatePicker/MonthDatePicker'; +import { ActivityPageMonthlyActivityTab } from '../components/ActivityPageMonthlyActivityTab/ActivityPageMonthlyActivityTab'; interface ActivityPageProps { date?: string; @@ -20,6 +22,7 @@ interface ActivityPageProps { enum ActivityPageTabs { Daily, Weekly, + Monthly } export const ActivityPage: React.FC = ({ @@ -46,6 +49,7 @@ export const ActivityPage: React.FC = ({ buttonType={ activeTab === value ? ButtonType.Primary : ButtonType.Secondary } + className='px-2' onClick={() => setActiveTab(value)} key={key} > @@ -71,6 +75,12 @@ export const ActivityPage: React.FC = ({ onWeekChange={setPickedSunday} /> )} + {activeTab === ActivityPageTabs.Monthly && ( + + )} {activeTab === ActivityPageTabs.Daily && ( @@ -83,6 +93,12 @@ export const ActivityPage: React.FC = ({ sundayDate={pickedSunday} /> )} + {activeTab === ActivityPageTabs.Monthly && ( + + )} ); }; diff --git a/src/popup/pages/OverallPage.tsx b/src/popup/pages/OverallPage.tsx index 77bfb7f..1614aea 100644 --- a/src/popup/pages/OverallPage.tsx +++ b/src/popup/pages/OverallPage.tsx @@ -54,6 +54,7 @@ export const OverallPage: React.FC = ({ activityTimeline={timelineEvents} filteredHostname={null} emptyHoursMarginCount={0} + description="Your web activity in last 6 hours." />
); diff --git a/src/popup/selectors/get-total-daily-activity.ts b/src/popup/selectors/get-total-daily-activity.ts index f83cdc7..b269143 100644 --- a/src/popup/selectors/get-total-daily-activity.ts +++ b/src/popup/selectors/get-total-daily-activity.ts @@ -5,8 +5,10 @@ export const getTotalDailyActivity = (store: TimeStore, date: Date) => { const todayIsoDate = getIsoDate(date); const todaysWebsitesUsage: Record = store[todayIsoDate] || {}; - return Object.values(todaysWebsitesUsage).reduce( - (sum, websiteTime) => sum + websiteTime, + const sumVal = Object.values(todaysWebsitesUsage).reduce( + (sum, websiteTime) => sum + Math.abs(websiteTime), 0 ); + + return sumVal }; diff --git a/src/popup/selectors/get-total-monthly-activity.ts b/src/popup/selectors/get-total-monthly-activity.ts new file mode 100644 index 0000000..62508a5 --- /dev/null +++ b/src/popup/selectors/get-total-monthly-activity.ts @@ -0,0 +1,10 @@ +import { get30DaysPriorDate } from '../../shared/utils/dates-helper'; + +import { TimeStore } from '../hooks/useTimeStore'; + +import { getTotalDailyActivity } from './get-total-daily-activity'; + +export const getTotalMonthlyActivity = (store: TimeStore, date = new Date()) => + get30DaysPriorDate(date).reduce((sum, date) => { + return sum + getTotalDailyActivity(store, date); + }, 0); diff --git a/src/shared/utils/dates-helper.ts b/src/shared/utils/dates-helper.ts index 72759ad..ada73fb 100644 --- a/src/shared/utils/dates-helper.ts +++ b/src/shared/utils/dates-helper.ts @@ -61,6 +61,22 @@ export const get7DaysPriorDate = < }); }; +export const get30DaysPriorDate = < + T extends (date: Date) => any = (date: Date) => Date +>( + date: Date, + map?: T +): ReturnType[] => { + const defaultMap = (date: Date) => new Date(date); + const monthEndDate = new Date(date); + + return new Array(30).fill(0).map((_, index) => { + monthEndDate.setDate(monthEndDate.getDate() - Number(index > 0)); + + return map?.(monthEndDate) ?? defaultMap(monthEndDate); + }); +}; + export const getDatesWeekSundayDate = (date: Date = new Date()) => { date.setDate(date.getDate() + date.getDay());