mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: improved user dropdown (#2969)
This commit is contained in:
1
src/assets/infinity.svg
Normal file
1
src/assets/infinity.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M494.9 96.01c-38.78 0-75.22 15.09-102.6 42.5L320 210.8L247.8 138.5c-27.41-27.41-63.84-42.5-102.6-42.5C65.11 96.01 0 161.1 0 241.1v29.75c0 80.03 65.11 145.1 145.1 145.1c38.78 0 75.22-15.09 102.6-42.5L320 301.3l72.23 72.25c27.41 27.41 63.84 42.5 102.6 42.5C574.9 416 640 350.9 640 270.9v-29.75C640 161.1 574.9 96.01 494.9 96.01zM202.5 328.3c-15.31 15.31-35.69 23.75-57.38 23.75C100.4 352 64 315.6 64 270.9v-29.75c0-44.72 36.41-81.13 81.14-81.13c21.69 0 42.06 8.438 57.38 23.75l72.23 72.25L202.5 328.3zM576 270.9c0 44.72-36.41 81.13-81.14 81.13c-21.69 0-42.06-8.438-57.38-23.75l-72.23-72.25l72.23-72.25c15.31-15.31 35.69-23.75 57.38-23.75C539.6 160 576 196.4 576 241.1V270.9z" fill="currentColor" /></svg>
|
After Width: | Height: | Size: 941 B |
@@ -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;
|
@@ -1,25 +1,39 @@
|
|||||||
import Transition from '@app/components/Transition';
|
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
||||||
import useClickOutside from '@app/hooks/useClickOutside';
|
|
||||||
import { useUser } from '@app/hooks/useUser';
|
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 { CogIcon, UserIcon } from '@heroicons/react/solid';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import type { LinkProps } from 'next/link';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRef, useState } from 'react';
|
import { forwardRef, Fragment } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
myprofile: 'Profile',
|
myprofile: 'Profile',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
|
requests: 'Requests',
|
||||||
signout: 'Sign Out',
|
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 UserDropdown = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const { user, revalidate } = useUser();
|
const { user, revalidate } = useUser();
|
||||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
|
||||||
useClickOutside(dropdownRef, () => setDropdownOpen(false));
|
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
const response = await axios.post('/api/v1/auth/logout');
|
const response = await axios.post('/api/v1/auth/logout');
|
||||||
@@ -30,14 +44,10 @@ const UserDropdown = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative ml-3">
|
<Menu as="div" className="relative ml-3">
|
||||||
<div>
|
<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"
|
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"
|
data-testid="user-menu"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -45,74 +55,108 @@ const UserDropdown = () => {
|
|||||||
src={user?.avatar}
|
src={user?.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
<Transition
|
||||||
show={isDropdownOpen}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-100"
|
enter="transition ease-out duration-100"
|
||||||
enterFrom="transform opacity-0 scale-95"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
enterTo="transform opacity-100 scale-100"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leave="transition ease-in duration-75"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
appear
|
||||||
>
|
>
|
||||||
<div
|
<Menu.Items className="absolute right-0 mt-2 w-72 origin-top-right rounded-md shadow-lg">
|
||||||
className="absolute right-0 mt-2 w-48 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">
|
||||||
ref={dropdownRef}
|
<div className="flex flex-col space-y-4 px-4 py-4">
|
||||||
>
|
<div className="flex items-center space-x-2">
|
||||||
<div
|
<img
|
||||||
className="rounded-md bg-gray-700 py-1 ring-1 ring-black ring-opacity-5"
|
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||||
role="menu"
|
src={user?.avatar}
|
||||||
aria-orientation="vertical"
|
alt=""
|
||||||
aria-labelledby="user-menu"
|
/>
|
||||||
>
|
<div className="flex min-w-0 flex-col">
|
||||||
<Link href={`/profile`}>
|
<span className="truncate text-xl font-semibold text-gray-200">
|
||||||
<a
|
{user?.displayName}
|
||||||
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"
|
</span>
|
||||||
role="menuitem"
|
<span className="truncate text-sm text-gray-400">
|
||||||
tabIndex={0}
|
{user?.email}
|
||||||
onKeyDown={(e) => {
|
</span>
|
||||||
if (e.key === 'Enter') {
|
</div>
|
||||||
setDropdownOpen(false);
|
</div>
|
||||||
}
|
{user && <MiniQuotaDisplay userId={user?.id} />}
|
||||||
}}
|
</div>
|
||||||
onClick={() => setDropdownOpen(false)}
|
<div className="p-1">
|
||||||
data-testid="user-menu-profile"
|
<Menu.Item>
|
||||||
>
|
{({ active }) => (
|
||||||
<UserIcon className="mr-2 inline h-5 w-5" />
|
<ForwardedLink
|
||||||
<span>{intl.formatMessage(messages.myprofile)}</span>
|
href={`/profile`}
|
||||||
</a>
|
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
|
||||||
</Link>
|
active
|
||||||
<Link href={`/profile/settings`}>
|
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
|
||||||
<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"
|
data-testid="user-menu-profile"
|
||||||
tabIndex={0}
|
>
|
||||||
onKeyDown={(e) => {
|
<UserIcon className="mr-2 inline h-5 w-5" />
|
||||||
if (e.key === 'Enter') {
|
<span>{intl.formatMessage(messages.myprofile)}</span>
|
||||||
setDropdownOpen(false);
|
</ForwardedLink>
|
||||||
}
|
)}
|
||||||
}}
|
</Menu.Item>
|
||||||
onClick={() => setDropdownOpen(false)}
|
<Menu.Item>
|
||||||
data-testid="user-menu-settings"
|
{({ active }) => (
|
||||||
>
|
<ForwardedLink
|
||||||
<CogIcon className="mr-2 inline h-5 w-5" />
|
href={`/users/${user?.id}/requests?filter=all`}
|
||||||
<span>{intl.formatMessage(messages.settings)}</span>
|
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
|
||||||
</a>
|
active
|
||||||
</Link>
|
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
|
||||||
<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"
|
data-testid="user-menu-settings"
|
||||||
role="menuitem"
|
>
|
||||||
onClick={() => logout()}
|
<ClockIcon className="mr-2 inline h-5 w-5" />
|
||||||
>
|
<span>{intl.formatMessage(messages.requests)}</span>
|
||||||
<LogoutIcon className="mr-2 inline h-5 w-5" />
|
</ForwardedLink>
|
||||||
<span>{intl.formatMessage(messages.signout)}</span>
|
)}
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -114,7 +114,10 @@
|
|||||||
"components.Layout.Sidebar.requests": "Requests",
|
"components.Layout.Sidebar.requests": "Requests",
|
||||||
"components.Layout.Sidebar.settings": "Settings",
|
"components.Layout.Sidebar.settings": "Settings",
|
||||||
"components.Layout.Sidebar.users": "Users",
|
"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.myprofile": "Profile",
|
||||||
|
"components.Layout.UserDropdown.requests": "Requests",
|
||||||
"components.Layout.UserDropdown.settings": "Settings",
|
"components.Layout.UserDropdown.settings": "Settings",
|
||||||
"components.Layout.UserDropdown.signout": "Sign Out",
|
"components.Layout.UserDropdown.signout": "Sign Out",
|
||||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
|
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
||||||
|
Reference in New Issue
Block a user