Skip to content

Commit

Permalink
finished options page
Browse files Browse the repository at this point in the history
  • Loading branch information
Iamsheye committed Sep 13, 2024
1 parent 6c9cdc1 commit 88ffef5
Show file tree
Hide file tree
Showing 11 changed files with 432 additions and 33 deletions.
5 changes: 4 additions & 1 deletion chrome-extension/lib/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ async function checkDailyLimit(siteId: string) {
try {
const sites = await siteStorage.get();
const site = sites.find(site => site.id === siteId);
if (site && site.dailyLimit !== null && site.dailyTime >= site.dailyLimit) {

if (!site) return;

if (site.dailyLimit !== null && site.dailyTime / 60 >= site.dailyLimit) {
await siteStorage.update(siteId, { isBlocked: true });
if (currentTabId) {
browser.tabs.sendMessage(currentTabId, { action: 'blockSite' });
Expand Down
64 changes: 63 additions & 1 deletion packages/tailwind-config/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,70 @@
import type { Config } from 'tailwindcss/types/config';
import { fontFamily } from 'tailwindcss/defaultTheme';

export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
extend: {
fontFamily: {
heading: ['var(--font-heading)', ...fontFamily.sans],
body: ['var(--font-body)', ...fontFamily.sans],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
xl: `calc(var(--radius) + 4px)`,
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: `calc(var(--radius) - 4px)`,
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [],
} as Omit<Config, 'content'>;
42 changes: 42 additions & 0 deletions packages/ui/lib/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import { cn } from '../utils';

const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} />
));
Card.displayName = 'Card';

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
);
CardHeader.displayName = 'CardHeader';

const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />

Check failure on line 18 in packages/ui/lib/components/Card.tsx

View workflow job for this annotation

GitHub Actions / eslint

Headings must have content and the content must be accessible by a screen reader
),
);
CardTitle.displayName = 'CardTitle';

const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
),
);
CardDescription.displayName = 'CardDescription';

const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
);
CardContent.displayName = 'CardContent';

const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
);
CardFooter.displayName = 'CardFooter';

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
9 changes: 9 additions & 0 deletions packages/ui/lib/components/Collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';

const Collapsible = CollapsiblePrimitive.Root;

const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;

const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;

export { Collapsible, CollapsibleTrigger, CollapsibleContent };
2 changes: 2 additions & 0 deletions packages/ui/lib/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './Button';
export * from './Input';
export * from './DropDownMenu';
export * from './Card';
export * from './Collapsible';
4 changes: 2 additions & 2 deletions packages/ui/lib/svgs/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Search, Settings, Clock, ArrowUpDown, EyeOff, Eye } from 'lucide-react';
import { Search, Settings, Clock, ArrowUpDown, EyeOff, Eye, ChevronDown, X, Download } from 'lucide-react';

export { Search, Settings, Clock, ArrowUpDown, EyeOff, Eye };
export { Search, Settings, Clock, ArrowUpDown, EyeOff, Eye, ChevronDown, X, Download };
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"deepmerge": "^4.3.1"
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
Expand Down
206 changes: 203 additions & 3 deletions pages/options/src/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,210 @@
import { useState } from 'react';
import { withErrorBoundary, withSuspense, useStorage, formatTime } from '@extension/shared';
import { siteStorage } from '@extension/storage';
import {
X,
Download,
Search,
Clock,
Input,
Button,
ArrowUpDown,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
CardContent,
Card,
ChevronDown,
EyeOff,
Eye,
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
} from '@extension/ui';
import '@src/Options.css';
import { withErrorBoundary, withSuspense } from '@extension/shared';

type SortOptions = 'time' | 'timeReverse' | 'alpha' | 'alphaReverse';

const Options = () => {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<SortOptions>('time');
const domains = useStorage(siteStorage);

const filteredAndSortedDomains = domains
.filter(domain => domain.domain.toLowerCase().includes(searchTerm.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'time') {
return b.dailyTime - a.dailyTime;
} else if (sortBy === 'alpha') {
return a.domain.localeCompare(b.domain);
} else if (sortBy === 'alphaReverse') {
return b.domain.localeCompare(a.domain);
} else {
// 'timeReverse'
return a.dailyTime - b.dailyTime;
}
});

const exportToCSV = () => {
const header = ['Domain', 'Is Blocked', 'Daily Limit', 'Date', 'Time Spent'].join(',');

const rows = domains.flatMap(site =>
site.dateTracking.map(tracking =>
[site.domain, site.isBlocked, site.dailyLimit ?? '', tracking.date, tracking.timeSpent].join(','),
),
);

const csvContent = [header, ...rows].join('\n');

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.setAttribute('download', 'social_detox-data.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};

const activeDropdownClassName = (value: string) => (sortBy === value ? 'bg-slate-500 text-white cursor-pointer' : '');

return (
<div className="App-container text-gray-900 bg-slate-50">
<img src={chrome.runtime.getURL('options/logo_horizontal.svg')} className="App-logo" alt="logo" />
<div className="container mx-auto p-4 max-w-4xl">
<div className="mb-6 flex items-center justify-center gap-4">
<h1 className="text-3xl font-bold text-center">Social Detox Settings</h1>
<Button variant="outline" size="sm" onClick={exportToCSV} className="flex items-center gap-1.5">
<Download size={18} />
<span>Export to CSV</span>
</Button>
</div>

<Card className="mb-6">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex gap-2 w-full">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<Input
type="text"
placeholder="Search domains"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="pl-8 pr-4 py-2 w-full"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<ArrowUpDown size={18} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-white">
<DropdownMenuItem className={activeDropdownClassName('time')} onClick={() => setSortBy('time')}>
Time (High to Low)
</DropdownMenuItem>
<DropdownMenuItem
className={activeDropdownClassName('timeReverse')}
onClick={() => setSortBy('timeReverse')}>
Time (Low to High)
</DropdownMenuItem>
<DropdownMenuItem className={activeDropdownClassName('alpha')} onClick={() => setSortBy('alpha')}>
A-Z
</DropdownMenuItem>
<DropdownMenuItem
className={activeDropdownClassName('alphaReverse')}
onClick={() => setSortBy('alphaReverse')}>
Z-A
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="px-4 py-2">
<div className="space-y-4">
{filteredAndSortedDomains.map(domain => (
<Collapsible key={domain.id} style={{ marginTop: 0 }} className="border-b last:border-b-0">
<div className="flex items-center justify-between py-2">
<div className="flex items-center">
<div>
<p className="font-medium">{domain.domain}</p>
<p className="text-sm text-gray-500 flex items-center">
<Clock size={14} className="mr-1" />
{formatTime(domain.dailyTime)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Input
type="number"
value={domain.dailyLimit || ''}
onChange={async e => {
const value = e.target.value;

await siteStorage.update(domain.id, {
dailyLimit: value ? parseInt(value) : null,
});
}}
min="0"
placeholder="Daily Limit (minutes)"
/>

<Button
size="sm"
variant="ghost"
className={`flex items-center gap-1 ${domain.isTrackingAllowed ? 'bg-gray-300 hover:bg-gray-400 text-gray-700' : 'text-white bg-green-500 hover:bg-green-600'}`}
onClick={async () => {
await siteStorage.update(domain.id, { isTrackingAllowed: !domain.isTrackingAllowed });
}}
disabled={domain.isBlocked}>
{domain.isTrackingAllowed ? <EyeOff size={14} /> : <Eye size={14} />}
<span>{domain.isTrackingAllowed ? 'Disable Tracking' : 'Enable Tracking'}</span>
</Button>
<Button
size="sm"
variant={domain.isBlocked ? 'secondary' : 'destructive'}
onClick={async () => {
await siteStorage.update(domain.id, { isBlocked: !domain.isBlocked });
}}
className={
domain.isBlocked
? 'bg-gray-300 text-gray-700 hover:bg-gray-400 flex items-center gap-1'
: 'flex items-center gap-1'
}>
<X />
{domain.isBlocked ? 'Unblock' : 'Block'}
</Button>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="flex items-center gap-1.5">
<span>View Tracking History</span>
<ChevronDown size={14} />
</Button>
</CollapsibleTrigger>
</div>
</div>
<CollapsibleContent>
<div className="my-2 px-6 space-y-2">
<h4 className="font-semibold text-sm">Tracking History</h4>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-600">Total Time Spent(last 30 days)</span>
<span>{formatTime(domain.dateTracking.reduce((acc, curr) => acc + curr.timeSpent, 0))}</span>
</div>
{domain.dateTracking.map(entry => (
<div key={entry.date} className="flex justify-between items-center text-sm">
<span className="text-gray-600">{new Date(entry.date).toDateString()}</span>
<span>{formatTime(entry.timeSpent)}</span>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
))}
</div>
</CardContent>
</Card>
</div>
);
};
Expand Down
Loading

0 comments on commit 88ffef5

Please sign in to comment.