Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/background/controller/overall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/popup/components/ActivityDatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const ActivityDatePicker: React.FC<ActivityDatePickerProps> = ({
<Button
buttonType={ButtonType.Secondary}
onClick={() => onDateChangeButtonClick(-1)}
className='px-4'
>
<Icon className="m-0 flex" type={IconType.LeftArrow} />
</Button>
Expand All @@ -50,6 +51,7 @@ export const ActivityDatePicker: React.FC<ActivityDatePickerProps> = ({
<Button
buttonType={ButtonType.Secondary}
onClick={() => onDateChangeButtonClick(1)}
className='px-4'
>
<Icon className="m-0 flex" type={IconType.RightArrow} />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const DailyActivityTab: React.FC<DailyActivityTabProps> = ({
title="Activity Timeline"
activityTimeline={activityTimeline}
filteredHostname={filteredHostname}
description="Your web activity timeline."
/>
</div>
<WebsiteActivityTable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from 'react';

import { TimeStore } from '../../hooks/useTimeStore';
import { get30DaysPriorDate, getIsoDate } from '../../../shared/utils/dates-helper';

import { TimeUsagePanel } from '../DailyTimeUsage/DailyTimeUsage';
import { WebsiteActivityTable } from '../WebsiteActivityTable/WebsiteActivityTable';
import { ActivityPageMonthlyActivityTabProps } from './types';
import { getTotalMonthlyActivity } from '../../selectors/get-total-monthly-activity';
import { MonthlyWebsiteActivityChart } from '../MonthlyWebsiteActivityChart/MonthlyWebsiteActivityChart';


export const ActivityPageMonthlyActivityTab: React.FC<ActivityPageMonthlyActivityTabProps> =
({ store, sundayDate }) => {
const [pickedDomain, setPickedDomain] = React.useState<null | string>(null);
const scrollToRef = React.useRef<HTMLDivElement | null>(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<string, number>),
[allMonthlyActivity]
);

const averageMonthlyActivity = React.useMemo(() => {
const averageMonthly =
getTotalMonthlyActivity(filteredWebsiteMonthActivity, sundayDate) / 7;
return averageMonthly;
}, [filteredWebsiteMonthActivity, sundayDate]);

const presentedPickedDomain = pickedDomain ?? 'All Websites';

return (
<div>
<TimeUsagePanel
title="Average Daily Activity"
time={averageMonthlyActivity}
/>
<div ref={scrollToRef}>
<MonthlyWebsiteActivityChart
store={filteredWebsiteMonthActivity}
sundayDate={sundayDate}
presentChartTitle={() =>
`Activity on ${presentedPickedDomain} per day`
}
/>
</div>
<WebsiteActivityTable
websiteTimeMap={totalWebsiteMonthlyActivity}
title={'Websites This Month'}
onDomainRowClicked={handleDomainRowClick}
/>
</div>
);
};
6 changes: 6 additions & 0 deletions src/popup/components/ActivityPageMonthlyActivityTab/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TimeStore } from '../../hooks/useTimeStore';

export interface ActivityPageMonthlyActivityTabProps {
store: TimeStore;
sundayDate: Date;
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const ActivityPageWeeklyActivityTab: React.FC<ActivityPageWeeklyActivityT
);
}, [allWeekActivity, pickedDomain]);


const totalWebsiteWeeklyActivity = React.useMemo(
() =>
Object.values(allWeekActivity).reduce((acc, dailyUsage) => {
Expand All @@ -61,6 +62,7 @@ export const ActivityPageWeeklyActivityTab: React.FC<ActivityPageWeeklyActivityT
[allWeekActivity]
);


const averageWeeklyActivity = React.useMemo(() => {
const averageWeekly =
getTotalWeeklyActivity(filteredWebsiteWeekActivity, sundayDate) / 7;
Expand Down
2 changes: 2 additions & 0 deletions src/popup/components/GeneralTimeline/GeneralTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const GeneralTimelineFC: React.FC<GeneralTimelineProps> = ({
activityTimeline,
title,
emptyHoursMarginCount = 2,
description,
}) => {
return (
<Panel>
<PanelHeader>
<Icon type={IconType.TimePast} />
{title}
{filteredHostname ? ` On ${filteredHostname}` : ''}
<span className='text-xs'> ({description})</span>
</PanelHeader>
<PanelBody
className={twMerge(
Expand Down
1 change: 1 addition & 0 deletions src/popup/components/GeneralTimeline/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface GeneralTimelineProps {
emptyHoursMarginCount?: number;
filteredHostname?: string | null;
activityTimeline: TimelineRecord[];
description: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { getIsoDate } from '../../../shared/utils/dates-helper';

import { GithubCalendarProps } from './types';

const INACTIVE_DAY_COLOR = '#cccccc';
const LOW_ACTIVITY_DAY_COLOR = '#839dde';
const MEDIUM_ACTIVITY_DAY_COLOR = '#4b76e3';
const HIGH_ACTIVITY_DAY_COLOR = '#103ba6';
const INACTIVE_DAY_COLOR = '#444444';
const LOW_ACTIVITY_DAY_COLOR = '#114A74';
const MEDIUM_ACTIVITY_DAY_COLOR = '#0073C1';
const HIGH_ACTIVITY_DAY_COLOR = '#5BC0FB';
const COLORS = [
INACTIVE_DAY_COLOR,
LOW_ACTIVITY_DAY_COLOR,
Expand Down Expand Up @@ -82,6 +82,14 @@ export const GithubCalendarWrapper: React.FC<GithubCalendarProps> = ({
<div className="calendar" ref={calendarRef} onClick={handleDateClick}>
{/* @ts-expect-error -- expected, this element does have props */}
<Calendar values={activity} panelColors={COLORS} />
<div className="flex items-center justify-end gap-1">
<span className="text-gray-400">Less</span>
<div className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: INACTIVE_DAY_COLOR }} />
<div className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: LOW_ACTIVITY_DAY_COLOR }} />
<div className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: MEDIUM_ACTIVITY_DAY_COLOR }} />
<div className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: HIGH_ACTIVITY_DAY_COLOR }} />
<span className="text-gray-400">More</span>
</div>
<ReactTooltip
id={REACT_TOOLTIP_ID}
delayShow={REACT_TOOLTIP_SHOW_DELAY_MS}
Expand Down
85 changes: 53 additions & 32 deletions src/popup/components/IgnoredDomainsSetting/IgnoredDomainSetting.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as React from 'react';
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';
Expand All @@ -14,27 +13,46 @@ export const IgnoredDomainSetting: React.FC = () => {
settings.ignoredHosts
);
const [domainToIgnore, setDomainToIgnore] = React.useState<string>('');
const [isDomainsListExpanded, setDomainsListExpanded] =
React.useState<boolean>(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]);

Expand All @@ -54,27 +72,32 @@ export const IgnoredDomainSetting: React.FC = () => {
);

const handleDomainToIgnoreChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setDomainToIgnore(e.target.value);
},
[]
);

const handleToggleDomainsListExpanded = React.useCallback(() => {
setDomainsListExpanded((prev) => !prev);
}, [setDomainsListExpanded]);
const handleKeyDown = ((event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if(event.key === 'Enter') {
event.preventDefault();
handleAddIgnoredDomain()
}
})

const { status, statusText } = state;
return (
<div className="p-2">
<PanelBody className="flex flex-col gap-2">
<p>You can hide unwanted websites to keep dashboards clean.</p>
<div className="flex justify-between items-end gap-2">
<label className="flex flex-col gap-1 w-full">
Domain
<Input
placeholder="e.g. google.com"
<TextArea
placeholder="e.g. google.com, bing.com (comma separated)"
value={domainToIgnore}
onChange={handleDomainToIgnoreChange}
onKeyDown={handleKeyDown}
/>
</label>
<Button
Expand All @@ -85,23 +108,21 @@ export const IgnoredDomainSetting: React.FC = () => {
Add
</Button>
</div>
<div className="flex justify-between items-end gap-2">
{status
&& (<p className="text-red-600">{statusText}</p>)
}
</div>
<div className="flex flex-col gap-2">
<a
href="#"
className="text-blue-500"
onClick={handleToggleDomainsListExpanded}
>
View all blacklisted domains
</a>
<div className={twMerge('hidden', isDomainsListExpanded && 'block')}>
<div className="block max-h-[125px] overflow-auto scroll-auto">
{!ignoredDomains.length && (
<p className="text-gray-500">No blacklisted domains</p>
)}
{ignoredDomains.map((domain) => (
<div key={domain} className="flex items-center gap-2">
<Icon
type={IconType.Close}
className="hover:text-neutral-400 cursor-pointer"
className="hover:text-neutral-400 cursor-pointer text-red-600"
onClick={() => handleRemoveIgnoredDomain(domain)}
/>
<span>{domain}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/popup/components/LimitsSetting/LimitsSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const LimitsSetting: React.FC = () => {
>
<Icon
type={IconType.Close}
className="hover:text-neutral-400 cursor-pointer"
className="hover:text-neutral-400 cursor-pointer text-red-600"
onClick={() => handleLimitRemove(domain)}
/>
<span className="flex-1">{domain}</span>
Expand Down
Loading