Skip to content

Commit

Permalink
dash: wired up blocked apps scheduling, tweaked ui, and added empty s…
Browse files Browse the repository at this point in the history
…tates
  • Loading branch information
kiahjh committed Dec 26, 2024
1 parent 9dcb06c commit f9a78ad
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 132 deletions.
3 changes: 3 additions & 0 deletions dash/app/src/components/routes/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ const UserRoute: React.FC = () => {
}
addNewBlockedApp={() => dispatch({ type: `addNewBlockedApp` })}
removeBlockedApp={(id) => dispatch({ type: `removeBlockedApp`, id })}
setBlockedAppSchedule={(id, schedule) =>
dispatch({ type: `setBlockedAppSchedule`, id, schedule })
}
/>
);
};
Expand Down
4 changes: 4 additions & 0 deletions dash/app/src/css/global.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions dash/app/src/reducers/user-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type Action =
| { type: 'updateNewBlockedAppIdentifier'; identifier: string }
| { type: 'removeBlockedApp'; id: UUID }
| { type: 'addNewBlockedApp' }
| { type: 'setBlockedAppSchedule'; id: UUID; schedule?: RuleSchedule }
| { type: 'setKeychainSchedule'; id: UUID; schedule?: RuleSchedule }
| { type: 'addKeychain'; keychain: UserKeychainSummary }
| { type: 'setAddingKeychainSchedule'; schedule?: RuleSchedule }
Expand Down Expand Up @@ -85,6 +86,14 @@ function reducer(state: State, action: Action): State | undefined {
(app) => app.id !== action.id,
);
return;
case `setBlockedAppSchedule`: {
if (!state.user.draft.blockedApps) return;
const blockedApp = state.user.draft.blockedApps.find((k) => k.id === action.id);
if (blockedApp) {
blockedApp.schedule = action.schedule;
}
return;
}
case `removeKeychain`:
state.user.draft.keychains = state.user.draft.keychains.filter(
(keychain) => keychain.id !== action.id,
Expand Down
95 changes: 95 additions & 0 deletions dash/components/src/Users/BlockedAppCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState } from 'react';
import cx from 'classnames';
import { defaults } from '@dash/types';
import { ChevronDownIcon, ClockIcon, TrashIcon } from '@heroicons/react/24/outline';
import type { RuleSchedule, BlockedApp } from '@dash/types';
import SchedulePicker from '../Keychains/schedule/SchedulePicker';

interface Props {
app: BlockedApp;
setSchedule(schedule?: RuleSchedule): void;
onDelete(): void;
}

const BlockedAppCard: React.FC<Props> = ({ app, setSchedule, onDelete }) => {
const [showSchedule, setShowSchedule] = useState(false);

return (
<div className="p-2.5 border border-slate-200 bg-white rounded-xl" key={app.id}>
<div className="flex items-center gap-3">
<i className="fa text-red-500 fa-ban p-1.5 bg-red-100/80 rounded-md" />
<div className="flex-grow overflow-hidden relative h-8 flex items-center">
<span className="font-semibold text-slate-600 whitespace-nowrap absolute left-0">
{app.identifier}
</span>
<div className="absolute right-0 top-0 h-full w-10 bg-gradient-to-r from-transparent to-white" />
</div>
{app.schedule ? (
<button
onClick={() => {
setShowSchedule(!showSchedule);
}}
className="flex items-center px-2 py-1 rounded-full transition-[background-color,transform] duration-200 active:scale-90 gap-1.5 bg-slate-200/50 hover:bg-slate-200 active:bg-slate-300 select-none"
>
<ChevronDownIcon
className={cx(
`w-3.5 h-3.5 shrink-0 text-slate-500 transition-transform duration-200`,
showSchedule && `rotate-180`,
)}
strokeWidth={2.5}
/>
<span className="text-sm text-slate-600 font-medium">Scheduled</span>
</button>
) : (
<button
onClick={() => {
setSchedule(defaults.ruleSchedule());
setShowSchedule(true);
}}
className="flex items-center px-2 py-1 rounded-full transition-[background-color,transform] duration-200 active:scale-90 gap-1.5 bg-slate-200/50 hover:bg-slate-200 active:bg-slate-300 select-none"
>
<ClockIcon
className="w-3.5 h-3.5 shrink-0 text-slate-500"
strokeWidth={2.5}
/>
<span className="text-sm text-slate-600 font-medium">Always blocked</span>
</button>
)}
<button
className="w-7 h-7 flex justify-center items-center rounded-full text-slate-400 hover:bg-slate-100 hover:text-red-500 transition-colors duration-150 shrink-0"
onClick={onDelete}
>
<TrashIcon className="w-4 h-4" strokeWidth={2.5} />
</button>
</div>
{app.schedule && (
<div
className={cx(
`flex justify-center items-center @container/schedule gap-2 transition-[height,margin-top,opacity] duration-200`,
showSchedule
? `h-auto mt-2 opacity-100`
: `h-0 mt-0 opacity-0 pointer-events-none`,
)}
>
<ClockIcon className="w-4 text-slate-400 shrink-0" strokeWidth={2.5} />
<SchedulePicker schedule={app.schedule} setSchedule={setSchedule} />
<button
onClick={() => {
setSchedule(undefined);
setShowSchedule(false);
}}
className="h-5 w-5 flex justify-center items-center rounded-full hover:scale-150 hover:bg-slate-100 transition-[transform,background-color] duration-200 group active:scale-100 active:bg-slate-300"
>
<TrashIcon
title="Remove schedule from keychain"
strokeWidth={2}
className="w-4 h-4 text-slate-400 group-hover:scale-75 transition-transform duration-200"
/>
</button>
</div>
)}
</div>
);
};

export default BlockedAppCard;
158 changes: 70 additions & 88 deletions dash/components/src/Users/EditUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@ import React from 'react';
import cx from 'classnames';
import { inflect } from '@shared/string';
import { TextInput, Button, Toggle, Label } from '@shared/components';
import { ClockIcon } from '@heroicons/react/24/outline';
import type { RuleSchedule, PlainTimeWindow, BlockedApp } from '@dash/types';
import type { Subcomponents, ConfirmableEntityAction, RequestState } from '@dash/types';
import { type RuleSchedule, type PlainTimeWindow } from '@dash/types';
import { Link } from 'react-router-dom';
import { NoSymbolIcon } from '@heroicons/react/24/outline';
import type {
Subcomponents,
ConfirmableEntityAction,
RequestState,
BlockedApp,
} from '@dash/types';
import type { UserKeychainSummary as Keychain } from '@dash/types';
import KeychainCard from '../Keychains/KeychainCard';
import { ConfirmDeleteEntity } from '../Modal';
import PageHeading from '../PageHeading';
import TimeInput from '../Forms/TimeInput';
import BetaBadge from '../BetaBadge';
import SchedulePicker from '../Keychains/schedule/SchedulePicker';
import EmptyState from '../EmptyState';
import AddKeychainDrawer from './AddKeychainDrawer';
import ConnectDeviceModal from './ConnectDeviceModal';
import UserDevice from './UserDevice';
import AddDeviceInstructions from './AddDeviceInstructions';
import BlockedAppCard from './BlockedAppCard';

interface Props {
id: string;
Expand Down Expand Up @@ -60,6 +67,7 @@ interface Props {
updateNewBlockedAppIdentifier(identifier: string): unknown;
addNewBlockedApp(): unknown;
removeBlockedApp(id: UUID): unknown;
setBlockedAppSchedule(id: UUID, schedule?: RuleSchedule): unknown;
}

const EditUser: React.FC<Props> = ({
Expand Down Expand Up @@ -105,6 +113,7 @@ const EditUser: React.FC<Props> = ({
updateNewBlockedAppIdentifier,
addNewBlockedApp,
removeBlockedApp,
setBlockedAppSchedule,
}) => {
if (isNew) {
return (
Expand Down Expand Up @@ -340,72 +349,31 @@ const EditUser: React.FC<Props> = ({
<BetaBadge />
</div>
{blockedApps.length === 0 ? (
<p className="text-center italic hidden text-slate-500 text-sm antialiased mt-2 mb-4">
No apps are currently blocked
</p>
<div className="flex flex-col items-center justify-center p-8 bg-slate-100 mt-2 rounded-2xl shadow-inner">
<NoSymbolIcon className="w-8 h-8 text-slate-300" strokeWidth={2} />
<h3 className="text-lg font-semibold text-slate-700 mt-2 mb-1">
No blocked apps
</h3>
<p className="text-slate-500 text-sm text-center">
Read more about what blocked apps are{` `}
<Link
to="https://gertrude.app/docs/app-blocking"
className="text-blue-500 font-medium underline"
>
here.
</Link>
</p>
</div>
) : (
<div className="gap-1.5 my-2 flex flex-col">
{blockedApps.map((app) => (
<div
className={cx(
`p-2.5 border border-slate-200 bg-white rounded-xl flex items-center @container/schedule`,
app.schedule &&
`flex-col min-[1070px]:flex-row gap-2 min-[1070px]:gap-0`,
)}
key={app.id}
>
<div
className={cx(
`flex items-center gap-3 flex-grow`,
app.schedule && `self-stretch min-[1070px]:self-auto`,
)}
>
<i className="fa text-red-500 fa-ban p-1.5 bg-red-100/80 rounded-md" />
<div className="flex-grow overflow-hidden relative h-8 flex items-center">
<span className="font-semibold text-slate-600 whitespace-nowrap absolute left-0">
{app.identifier}
</span>
<div className="absolute right-0 top-0 h-full w-10 bg-gradient-to-r from-transparent to-white" />
</div>
</div>
<div
className={cx(
`flex items-center gap-2 ml-2 shrink-0`,
app.schedule && `self-end min-[1070px]:self-auto`,
)}
>
{app.schedule ? (
<SchedulePicker
schedule={app.schedule}
setSchedule={() => {
alert(`todo`);
}}
small
/>
) : (
<button
onClick={() => {
alert(`todo`);
}}
className="flex items-center px-2 py-1 rounded-full transition-[background-color,transform] duration-200 active:scale-90 gap-1.5 bg-slate-200/50 hover:bg-slate-200 active:bg-slate-300 select-none"
>
<ClockIcon
className={cx(`w-3.5 h-3.5 shrink-0 text-slate-500`)}
strokeWidth={2.5}
/>
<span className="text-sm text-slate-600 font-medium">
Always active
</span>
</button>
)}
<button
className="w-7 h-7 flex justify-center items-center rounded-full text-slate-500 hover:bg-slate-100 hover:text-red-500 transition-colors duration-150 shrink-0"
onClick={() => removeBlockedApp(app.id)}
>
<i className="fa fa-trash text-sm" />
</button>
</div>
</div>
<BlockedAppCard
app={app}
setSchedule={(schedule) =>
setBlockedAppSchedule(app.id, schedule)
}
onDelete={() => removeBlockedApp(app.id)}
/>
))}
</div>
)}
Expand Down Expand Up @@ -442,28 +410,42 @@ const EditUser: React.FC<Props> = ({
<div className="mt-12 max-w-3xl">
<h2 className="text-lg font-bold text-slate-700 mb-2">Keychains</h2>
<div className="py-3 flex flex-col space-y-4">
{keychains.map((keychain) => (
<KeychainCard
mode="assign_to_child"
schedule={keychain.schedule}
key={keychain.id}
name={keychain.name}
description={keychain.description}
numKeys={keychain.numKeys}
isPublic={keychain.isPublic}
onRemove={() => removeKeychain(keychain.id)}
setSchedule={(schedule) =>
setAssignedKeychainSchedule(keychain.id, schedule)
}
{keychains.length === 0 ? (
<EmptyState
heading={`No keychains`}
secondaryText={`By default, all internet access is blocked for this child until you assign a keychain.`}
icon={`key`}
buttonText={`Add keychain`}
action={onAddKeychainClicked}
/>
))}
<button
className="mt-5 text-violet-700 font-medium px-7 py-2 rounded-lg hover:bg-violet-100 self-end transition-colors duration-100"
onClick={onAddKeychainClicked}
>
<i className="fa fa-plus mr-2" />
Add keychain
</button>
) : (
<>
{keychains.map((keychain) => (
<KeychainCard
mode="assign_to_child"
schedule={keychain.schedule}
key={keychain.id}
name={keychain.name}
description={keychain.description}
numKeys={keychain.numKeys}
isPublic={keychain.isPublic}
onRemove={() => removeKeychain(keychain.id)}
setSchedule={(schedule) =>
setAssignedKeychainSchedule(keychain.id, schedule)
}
/>
))}
<Button
type="button"
onClick={onAddKeychainClicked}
color="secondary"
className="xs:self-end"
>
<i className="fa fa-plus mr-2" />
Add keychain
</Button>
</>
)}
</div>
</div>
</>
Expand Down
Loading

0 comments on commit f9a78ad

Please sign in to comment.