Skip to content

Commit

Permalink
Create timeout modal
Browse files Browse the repository at this point in the history
  • Loading branch information
JorWo committed Mar 30, 2024
1 parent df7c92e commit 9f169a1
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 35 deletions.
2 changes: 1 addition & 1 deletion ui/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const config: Config = {
customExportConditions: []
},
setupFilesAfterEnv: [
'<rootDir>/tests/setupJest.tsx'
'<rootDir>/tests/setupJest.ts'
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleDirectories: ['node_modules', '<rootDir>'],
Expand Down
2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
Expand All @@ -25,6 +26,7 @@
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"react-idle-timer": "^5.7.2",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"uniqid": "^5.4.0"
Expand Down
68 changes: 36 additions & 32 deletions ui/src/components/layout/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,51 @@ import { getCurrentUser } from '@/access/AuthenticationService';
import MobileNavbar from './MobileNavbar';
import { NavLinks } from './NavLinks';
import Role from '@/access/Role';
import TimeoutModal from '@/components/modal/TimeoutModal';

const Navbar = async () => {
const currentUser = await getCurrentUser();

return (
<nav className="bg-white border-b-[1px] pointer-events-auto sticky top-0">
<div className="container py-2">
<div className="flex justify-between">
<Link href="/" className="lg:inline hidden">
<Image
src="/uhgroupings/uh-groupings-logo.svg"
alt="UH Groupings Logo"
width={256}
height={256} />
</Link>
<div className="flex lg:hidden">
<MobileNavbar currentUser={currentUser} />
<Link href="/">
<Image
src="/uhgroupings/uh-groupings-logo-large.svg"
<>
<TimeoutModal currentUser={currentUser} />
<nav className="bg-white border-b-[1px] pointer-events-auto sticky top-0">
<div className="container py-2">
<div className="flex justify-between">
<Link href="/" className="lg:inline hidden">
<Image
src="/uhgroupings/uh-groupings-logo.svg"
alt="UH Groupings Logo"
width={56}
height={56} />
width={256}
height={256} />
</Link>
</div>
<div className="text-lg text-uh-black my-auto lg:space-x-5">
{NavLinks
.filter(navLink =>
currentUser.roles.includes(Role.ADMIN) || currentUser.roles.includes(navLink.role))
.map(navLink =>
<Link
href={navLink.link}
key={navLink.name}
className="hover:text-uh-teal lg:inline hidden">
{navLink.name}
</Link>)}
<LoginButton currentUser={currentUser} />
<div className="flex lg:hidden">
<MobileNavbar currentUser={currentUser} />
<Link href="/">
<Image
src="/uhgroupings/uh-groupings-logo-large.svg"
alt="UH Groupings Logo"
width={56}
height={56} />
</Link>
</div>
<div className="text-lg text-uh-black my-auto lg:space-x-5">
{NavLinks
.filter(navLink =>
currentUser.roles.includes(Role.ADMIN) || currentUser.roles.includes(navLink.role))
.map(navLink =>
<Link
href={navLink.link}
key={navLink.name}
className="hover:text-uh-teal lg:inline hidden">
{navLink.name}
</Link>)}
<LoginButton currentUser={currentUser} />
</div>
</div>
</div>
</div>
</nav>
</nav>
</>
);
}

Expand Down
92 changes: 92 additions & 0 deletions ui/src/components/modal/TimeoutModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use client';

import { useEffect, useState } from 'react';
import {
AlertDialog,
AlertDialogHeader,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel
} from '@/components/ui/alert-dialog';
import { useIdleTimer } from 'react-idle-timer';
import { logout } from '@/access/AuthenticationService';
import User from '@/access/User';
import Role from '@/access/Role';

const timeout = 1000 * 60 * 30; // Total timeout - 30 minutes in milliseconds
const promptBeforeIdle = 1000 * 60 * 5; // Time prior to timeout until modal opens - 5 minutes in milliseconds

const TimeoutModal = ({
currentUser
}: {
currentUser: User
}) => {
const [open, setOpen] = useState(false);
const [remainingTime, setRemainingTime] = useState<number>(timeout);

const { activate, getRemainingTime } = useIdleTimer({
onIdle: () => logout(),
onPrompt: () => setOpen(true),
timeout,
promptBeforeIdle,
throttle: 500,
disabled: !currentUser.roles.includes(Role.UH)
});

useEffect(() => {
const interval = setInterval(() => {
setRemainingTime(getRemainingTime());
}, 500);

return () => {
clearInterval(interval);
};
});

/**
* Closes the modal and resets the timer.
*/
const close = () => {
activate();
setOpen(false);
};

/**
* Convert miliseconds into mm:ss string format.
*
* @param ms - the number of milliseconds
*
* @returns the mm:ss formatted string
*/
const formatTime = (ms: number) => {
const date = new Date(ms);
const minutes = date.getMinutes();
const seconds = date.getSeconds();

return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};

return (
<AlertDialog open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Inactivity Warning</AlertDialogTitle>
<AlertDialogDescription>
Warning! This session will expire soon.
Time remaining:
<span className="text-text-color"> {formatTime(remainingTime)}.</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => close()}>Stay logged in</AlertDialogCancel>
<AlertDialogAction onClick={() => logout()}>Log off now</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

export default TimeoutModal;
141 changes: 141 additions & 0 deletions ui/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use client'

import * as React from 'react'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'

import { cn } from '@/components/ui/utils'
import { buttonVariants } from '@/components/ui/button'

const AlertDialog = AlertDialogPrimitive.Root

const AlertDialogTrigger = AlertDialogPrimitive.Trigger

const AlertDialogPortal = AlertDialogPrimitive.Portal

const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white py-4 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-[0.25rem] dark:border-slate-800 dark:bg-slate-950',
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = 'AlertDialogHeader'

const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 px-4 pt-4 border-t',
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = 'AlertDialogFooter'

const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-xl px-4 pb-4 border-b font-normal text-text-color', className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName

const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-base px-4 py-2', className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName

const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName

const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'secondary' }),
'mt-2 sm:mt-0',
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName

export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
3 changes: 1 addition & 2 deletions ui/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ const buttonVariants = cva(
destructive: `bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50
dark:hover:bg-red-900/90`,
outline: `border border-green-blue bg-white hover:bg-green-blue hover:text-white text-uh-teal`,
secondary: `bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50
dark:hover:bg-slate-800/80`,
secondary: `border border-transparent bg-[#f8f9fa] text-[#212529] hover:bg-uh-blue4 hover:text-white`,
ghost: `hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50`,
link: `text-slate-900 underline-offset-4 hover:underline dark:text-slate-50`,
},
Expand Down
1 change: 1 addition & 0 deletions ui/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const config = {
extend: {
colors: {
'green-blue': '#004e59',
'uh-blue4': '#4c90a8',
'uh-black': '#212121',
'uh-teal': '#0d7078',
'seafoam': '#e3f2ef',
Expand Down
Loading

0 comments on commit 9f169a1

Please sign in to comment.