Skip to content

Commit

Permalink
feat: improved user dropdown (#2969)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan Cohen committed Aug 24, 2022
1 parent f5e5016 commit 67f3a38
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 70 deletions.
1 change: 1 addition & 0 deletions src/assets/infinity.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions src/components/Layout/UserDropdown/MiniQuotaDisplay/index.tsx
@@ -0,0 +1,93 @@
import Infinity from '@app/assets/infinity.svg';
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
import ProgressCircle from '@app/components/Common/ProgressCircle';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';

const messages = defineMessages({
movierequests: 'Movie Requests',
seriesrequests: 'Series Requests',
});

type MiniQuotaDisplayProps = {
userId: number;
};

const MiniQuotaDisplay = ({ userId }: MiniQuotaDisplayProps) => {
const intl = useIntl();
const { data, error } = useSWR<QuotaResponse>(`/api/v1/user/${userId}/quota`);

if (error) {
return null;
}

if (!data && !error) {
return <SmallLoadingSpinner />;
}

return (
<>
{((data?.movie.limit ?? 0) !== 0 || (data?.tv.limit ?? 0) !== 0) && (
<div className="flex">
<div className="flex basis-1/2 flex-col space-y-2">
<div className="text-sm text-gray-200">
{intl.formatMessage(messages.movierequests)}
</div>
<div className="flex h-full items-center space-x-2 text-gray-200">
{data?.movie.limit ?? 0 > 0 ? (
<>
<ProgressCircle
className="h-8 w-8"
progress={Math.round(
((data?.movie.remaining ?? 0) /
(data?.movie.limit ?? 1)) *
100
)}
useHeatLevel
/>
<span className="text-lg font-bold">
{data?.movie.remaining} / {data?.movie.limit}
</span>
</>
) : (
<>
<Infinity className="w-7" />
<span className="font-bold">Unlimited</span>
</>
)}
</div>
</div>
<div className="flex basis-1/2 flex-col space-y-2">
<div className="text-sm text-gray-200">
{intl.formatMessage(messages.seriesrequests)}
</div>
<div className="flex h-full items-center space-x-2 text-gray-200">
{data?.tv.limit ?? 0 > 0 ? (
<>
<ProgressCircle
className="h-8 w-8"
progress={Math.round(
((data?.tv.remaining ?? 0) / (data?.tv.limit ?? 1)) * 100
)}
useHeatLevel
/>
<span className="text-lg font-bold text-gray-200">
{data?.tv.remaining} / {data?.tv.limit}
</span>
</>
) : (
<>
<Infinity className="w-7" />
<span className="font-bold">Unlimited</span>
</>
)}
</div>
</div>
</div>
)}
</>
);
};

export default MiniQuotaDisplay;
184 changes: 114 additions & 70 deletions src/components/Layout/UserDropdown/index.tsx
@@ -1,25 +1,39 @@
import Transition from '@app/components/Transition';
import useClickOutside from '@app/hooks/useClickOutside';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser';
import { LogoutIcon } from '@heroicons/react/outline';
import { Menu, Transition } from '@headlessui/react';
import { ClockIcon, LogoutIcon } from '@heroicons/react/outline';
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import axios from 'axios';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { useRef, useState } from 'react';
import { forwardRef, Fragment } from 'react';
import { defineMessages, useIntl } from 'react-intl';

const messages = defineMessages({
myprofile: 'Profile',
settings: 'Settings',
requests: 'Requests',
signout: 'Sign Out',
});

const ForwardedLink = forwardRef<
HTMLAnchorElement,
LinkProps & React.ComponentPropsWithoutRef<'a'>
>(({ href, children, ...rest }, ref) => {
return (
<Link href={href}>
<a ref={ref} {...rest}>
{children}
</a>
</Link>
);
});

ForwardedLink.displayName = 'ForwardedLink';

const UserDropdown = () => {
const intl = useIntl();
const dropdownRef = useRef<HTMLDivElement>(null);
const { user, revalidate } = useUser();
const [isDropdownOpen, setDropdownOpen] = useState(false);
useClickOutside(dropdownRef, () => setDropdownOpen(false));

const logout = async () => {
const response = await axios.post('/api/v1/auth/logout');
Expand All @@ -30,89 +44,119 @@ const UserDropdown = () => {
};

return (
<div className="relative ml-3">
<Menu as="div" className="relative ml-3">
<div>
<button
<Menu.Button
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
id="user-menu"
aria-label="User menu"
aria-haspopup="true"
onClick={() => setDropdownOpen(true)}
data-testid="user-menu"
>
<img
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar}
alt=""
/>
</button>
</Menu.Button>
</div>
<Transition
show={isDropdownOpen}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
appear
>
<div
className="absolute right-0 mt-2 w-48 origin-top-right rounded-md shadow-lg"
ref={dropdownRef}
>
<div
className="rounded-md bg-gray-700 py-1 ring-1 ring-black ring-opacity-5"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu"
>
<Link href={`/profile`}>
<a
className="flex items-center px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600"
role="menuitem"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setDropdownOpen(false);
}
}}
onClick={() => setDropdownOpen(false)}
data-testid="user-menu-profile"
>
<UserIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.myprofile)}</span>
</a>
</Link>
<Link href={`/profile/settings`}>
<a
className="flex items-center px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600"
role="menuitem"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setDropdownOpen(false);
}
}}
onClick={() => setDropdownOpen(false)}
data-testid="user-menu-settings"
>
<CogIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.settings)}</span>
</a>
</Link>
<a
href="#"
className="flex items-center px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out hover:bg-gray-600"
role="menuitem"
onClick={() => logout()}
>
<LogoutIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.signout)}</span>
</a>
<Menu.Items className="absolute right-0 mt-2 w-72 origin-top-right rounded-md shadow-lg">
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
<div className="flex flex-col space-y-4 px-4 py-4">
<div className="flex items-center space-x-2">
<img
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar}
alt=""
/>
<div className="flex min-w-0 flex-col">
<span className="truncate text-xl font-semibold text-gray-200">
{user?.displayName}
</span>
<span className="truncate text-sm text-gray-400">
{user?.email}
</span>
</div>
</div>
{user && <MiniQuotaDisplay userId={user?.id} />}
</div>
<div className="p-1">
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/profile`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
data-testid="user-menu-profile"
>
<UserIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.myprofile)}</span>
</ForwardedLink>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/users/${user?.id}/requests?filter=all`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
data-testid="user-menu-settings"
>
<ClockIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.requests)}</span>
</ForwardedLink>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/profile/settings`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
data-testid="user-menu-settings"
>
<CogIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.settings)}</span>
</ForwardedLink>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
href="#"
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
onClick={() => logout()}
>
<LogoutIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.signout)}</span>
</a>
)}
</Menu.Item>
</div>
</div>
</div>
</Menu.Items>
</Transition>
</div>
</Menu>
);
};

Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locale/en.json
Expand Up @@ -114,7 +114,10 @@
"components.Layout.Sidebar.requests": "Requests",
"components.Layout.Sidebar.settings": "Settings",
"components.Layout.Sidebar.users": "Users",
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Movie Requests",
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Series Requests",
"components.Layout.UserDropdown.myprofile": "Profile",
"components.Layout.UserDropdown.requests": "Requests",
"components.Layout.UserDropdown.settings": "Settings",
"components.Layout.UserDropdown.signout": "Sign Out",
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
Expand Down
1 change: 1 addition & 0 deletions tailwind.config.js
@@ -1,6 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const defaultTheme = require('tailwindcss/defaultTheme');

/** @type {import('tailwindcss').Config} */
module.exports = {
mode: 'jit',
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
Expand Down

0 comments on commit 67f3a38

Please sign in to comment.