Skip to content
Merged
8 changes: 0 additions & 8 deletions src/components/Layout/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Badge from '@app/components/Common/Badge';
import UserWarnings from '@app/components/Layout/UserWarnings';
import VersionStatus from '@app/components/Layout/VersionStatus';
import useClickOutside from '@app/hooks/useClickOutside';
import { Permission, useUser } from '@app/hooks/useUser';
Expand Down Expand Up @@ -233,10 +232,6 @@ const Sidebar = ({
);
})}
</nav>
<div className="px-2">
<UserWarnings onClick={() => setClosed()} />
</div>

{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus onClick={() => setClosed()} />
Expand Down Expand Up @@ -322,9 +317,6 @@ const Sidebar = ({
);
})}
</nav>
<div className="px-2">
<UserWarnings />
</div>
{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus />
Expand Down
85 changes: 47 additions & 38 deletions src/components/Layout/UserWarnings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,72 @@
import AddEmailModal from '@app/components/Login/AddEmailModal';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import {
ChevronRightIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
import { useState } from 'react';
import { useIntl } from 'react-intl';

const messages = defineMessages('components.Layout.UserWarnings', {
emailRequired: 'An email address is required.',
emailInvalid: 'Email address is invalid.',
passwordRequired: 'A password is required.',
profileIncomplete: 'Profile is incomplete',
});

interface UserWarningsProps {
onClick?: () => void;
}

const UserWarnings: React.FC<UserWarningsProps> = ({ onClick }) => {
const UserWarnings: React.FC = () => {
const intl = useIntl();
const { user } = useUser();
//check if a user has warnings
const { user, revalidate } = useUser();
const [showEmailModal, setShowEmailModal] = useState(false);

if (!user || !user.warnings || user.warnings.length === 0) {
return null;
}

let res = null;
let warningText = '';
let showSetEmail = false;

user.warnings.forEach((warning) => {
let link = '';
let warningText = '';
let warningTitle = '';
for (const warning of user.warnings) {
switch (warning) {
case 'userEmailRequired':
link = '/profile/settings/';
warningTitle = 'Profile is incomplete';
warningText = intl.formatMessage(messages.emailRequired);
showSetEmail = true;
break;
}
}

if (!warningText) {
return null;
}

res = (
<Link
href={link}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' && onClick) {
onClick();
}
}}
role="button"
tabIndex={0}
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
return (
<>
{showEmailModal && (
<AddEmailModal
onClose={() => setShowEmailModal(false)}
onSave={() => {
setShowEmailModal(false);
revalidate();
}}
/>
)}
<button
onClick={showSetEmail ? () => setShowEmailModal(true) : undefined}
className="service-error-banner mb-4 w-full cursor-pointer transition duration-300 hover:border-yellow-400 hover:bg-yellow-500/30"
>
<ExclamationTriangleIcon className="h-6 w-6" />
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
<span className="font-bold">{warningTitle}</span>
<span className="truncate">{warningText}</span>
</div>
</Link>
);
});

return res;
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0" />
<span className="flex-1 text-left">
<span className="font-bold">
{intl.formatMessage(messages.profileIncomplete)}
</span>
{': '}
{warningText}
</span>
<ChevronRightIcon className="h-5 w-5 flex-shrink-0" />
</button>
</>
);
};

export default UserWarnings;
6 changes: 5 additions & 1 deletion src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PullToRefresh from '@app/components/Layout/PullToRefresh';
import SearchInput from '@app/components/Layout/SearchInput';
import Sidebar from '@app/components/Layout/Sidebar';
import UserDropdown from '@app/components/Layout/UserDropdown';
import UserWarnings from '@app/components/Layout/UserWarnings';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
Expand Down Expand Up @@ -124,7 +125,10 @@ const Layout = ({ children }: LayoutProps) => {

<main className="relative top-16 z-0 focus:outline-none" tabIndex={0}>
<div className="mb-6">
<div className="max-w-8xl mx-auto px-4">{children}</div>
<div className="max-w-8xl mx-auto px-4">
<UserWarnings />
{children}
</div>
</div>
</main>
</div>
Expand Down
130 changes: 72 additions & 58 deletions src/components/Login/AddEmailModal.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { ApiErrorCode } from '@server/constants/error';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import { mutate } from 'swr';
import validator from 'validator';
import * as Yup from 'yup';

const messages = defineMessages('components.Login', {
title: 'Add Email',
description:
'Since this is your first time logging into {applicationName}, you are required to add a valid email address.',
'Add a valid email address to complete your profile. This will be used for notifications and local sign-in.',
email: 'Email address',
emailAlreadyTaken: 'This email is already in use.',
validationEmailRequired: 'You must provide an email',
validationEmailFormat: 'Invalid email',
saving: 'Adding…',
save: 'Add',
saveFailed: 'Something went wrong while saving.',
});

interface AddEmailModalProps {
username: string;
password: string;
onClose: () => void;
onSave: () => void;
}

const AddEmailModal: React.FC<AddEmailModalProps> = ({
onClose,
username,
password,
onSave,
}) => {
const AddEmailModal: React.FC<AddEmailModalProps> = ({ onClose, onSave }) => {
const intl = useIntl();
const settings = useSettings();
const { addToast } = useToasts();
const { user } = useUser();

const EmailSettingsSchema = Yup.object().shape({
const EmailSchema = Yup.object().shape({
email: Yup.string()
.test(
'email',
Expand All @@ -51,66 +50,81 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
show
enter="transition ease-in-out duration-300 transform opacity-0"
enterFrom="opacity-0"
enterTo="opacuty-100"
enterTo="opacity-100"
leave="transition ease-in-out duration-300 transform opacity-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Formik
initialValues={{
email: '',
}}
validationSchema={EmailSettingsSchema}
initialValues={{ email: '' }}
validationSchema={EmailSchema}
onSubmit={async (values) => {
if (!user?.id) {
addToast(intl.formatMessage(messages.saveFailed), {
autoDismiss: true,
appearance: 'error',
});
return;
}

try {
await axios.post('/api/v1/auth/jellyfin', {
username: username,
password: password,
const { data: current } = await axios.get(
`/api/v1/user/${user.id}/settings/main`
);
await axios.post(`/api/v1/user/${user.id}/settings/main`, {
...current,
email: values.email,
});

await mutate(`/api/v1/user/${user.id}/settings/main`);
onSave();
} catch {
// set error here
} catch (e) {
addToast(
intl.formatMessage(
axios.isAxiosError(e) &&
e.response?.data?.message === ApiErrorCode.InvalidEmail
? messages.emailAlreadyTaken
: messages.saveFailed
),
{
autoDismiss: true,
appearance: 'error',
}
);
}
}}
>
{({ errors, touched, handleSubmit, isSubmitting, isValid }) => {
return (
<Modal
onCancel={onClose}
okButtonType="primary"
okText={
isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)
}
okDisabled={isSubmitting || !isValid}
onOk={() => handleSubmit()}
title={intl.formatMessage(messages.title)}
>
{intl.formatMessage(messages.description, {
applicationName: settings.currentSettings.applicationTitle,
})}
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
</label>
<div className="mb-2 mt-1 sm:col-span-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
<Field
id="email"
name="email"
type="text"
placeholder={intl.formatMessage(messages.email)}
/>
</div>
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
{({ errors, touched, handleSubmit, isSubmitting, isValid }) => (
<Modal
onCancel={onClose}
okButtonType="primary"
okText={
isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)
}
okDisabled={isSubmitting || !isValid}
onOk={() => handleSubmit()}
title={intl.formatMessage(messages.title)}
>
{intl.formatMessage(messages.description)}
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
</label>
<div className="mb-2 mt-1 sm:col-span-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
<Field
id="email"
name="email"
type="text"
placeholder={intl.formatMessage(messages.email)}
/>
</div>
</Modal>
);
}}
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
</div>
</Modal>
)}
</Formik>
</Transition>
);
Expand Down
Loading
Loading