From 718c64f973601130a2f03ca1549a6af86b6dde21 Mon Sep 17 00:00:00 2001 From: 0xsysr3ll <0xsysr3ll@pm.me> Date: Mon, 21 Jul 2025 23:57:02 +0200 Subject: [PATCH] feat(plex-auth): enhanced layout and styling for better UX --- src/components/Login/PlexPinEntry.tsx | 159 ++++++++++++++----- src/components/Login/PlexProfileSelector.tsx | 39 +++-- src/components/Login/index.tsx | 34 +++- 3 files changed, 176 insertions(+), 56 deletions(-) diff --git a/src/components/Login/PlexPinEntry.tsx b/src/components/Login/PlexPinEntry.tsx index 78f7658d..036208d3 100644 --- a/src/components/Login/PlexPinEntry.tsx +++ b/src/components/Login/PlexPinEntry.tsx @@ -1,5 +1,7 @@ import Button from '@app/components/Common/Button'; import defineMessages from '@app/utils/defineMessages'; +import { LockClosedIcon } from '@heroicons/react/24/solid'; +import Image from 'next/image'; import { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -16,13 +18,20 @@ const messages = defineMessages('components.Login.PlexPinEntry', { interface PlexPinEntryProps { profileId: string; profileName: string; + profileThumb?: string | null; + isProtected?: boolean; + isMainUser?: boolean; + error?: string | null; onSubmit: (pin: string) => Promise; onCancel: () => void; - error?: string | null; } const PlexPinEntry = ({ profileName, + profileThumb, + isProtected, + isMainUser, + error, onSubmit, onCancel, }: PlexPinEntryProps) => { @@ -60,7 +69,6 @@ const PlexPinEntry = ({ const handleChange = (e: React.ChangeEvent) => { const value = e.target.value.replace(/\D/g, ''); setPin(value); - if (value.length === 4 && !isSubmitting) { handleSubmit(value); } @@ -70,46 +78,115 @@ const PlexPinEntry = ({ e.target.select(); }; + // PIN boxes rendering + const pinDigits = pin.split('').slice(0, 4); + const boxes = Array.from({ length: 4 }, (_, i) => ( +
+ {pinDigits[i] ? '•' : ''} +
+ )); + return ( -
-

- {intl.formatMessage(messages.pinRequired)} -

-

- {intl.formatMessage(messages.pinDescription)}{' '} - {profileName} -

- -
- -
- -
- - +
+
+ {/* Avatar */} +
+ {profileThumb ? ( + {profileName} + ) : ( + + {profileName?.[0] || '?'} + + )} +
+ {/* Icons */} +
+ {isProtected && ( + + + + )} + {isMainUser && ( + + + + + + )} +
+

+ {profileName} +

+

+ {intl.formatMessage(messages.pinRequired)} +

+

+ {intl.formatMessage(messages.pinDescription)} +

+
+ {boxes} + {/* Visually hidden input for keyboard entry */} + +
+ {error && ( +
+ {error} +
+ )} +
+ + +
); diff --git a/src/components/Login/PlexProfileSelector.tsx b/src/components/Login/PlexProfileSelector.tsx index dc791c32..c5c87801 100644 --- a/src/components/Login/PlexProfileSelector.tsx +++ b/src/components/Login/PlexProfileSelector.tsx @@ -35,7 +35,6 @@ const PlexProfileSelector = ({ ); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); - const [pinError, setPinError] = useState(null); const [showPinEntry, setShowPinEntry] = useState(false); const [selectedProfile, setSelectedProfile] = useState( null @@ -62,14 +61,13 @@ const PlexProfileSelector = ({ const handlePinSubmit = async (pin: string) => { if (!selectedProfileId) return; - await onProfileSelected(selectedProfileId, pin, setPinError); + await onProfileSelected(selectedProfileId, pin); }; const handlePinCancel = () => { setShowPinEntry(false); setSelectedProfile(null); setSelectedProfileId(null); - setPinError(null); }; if (showPinEntry && selectedProfile && selectedProfileId) { @@ -81,9 +79,11 @@ const PlexProfileSelector = ({ selectedProfile.username || intl.formatMessage(messages.profile) } + profileThumb={selectedProfile.thumb} + isProtected={selectedProfile.protected} + isMainUser={selectedProfile.isMainUser} onSubmit={handlePinSubmit} onCancel={handlePinCancel} - error={pinError} /> ); } @@ -110,7 +110,7 @@ const PlexProfileSelector = ({
)} -
+
{profiles.map((profile) => ( ))}
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 20f6b922..e806c747 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -57,6 +57,9 @@ const Login = () => { const [showPinEntry, setShowPinEntry] = useState(false); const [pinProfileId, setPinProfileId] = useState(null); const [pinProfileName, setPinProfileName] = useState(null); + const [pinProfileThumb, setPinProfileThumb] = useState(null); + const [pinIsProtected, setPinIsProtected] = useState(false); + const [pinIsMainUser, setPinIsMainUser] = useState(false); const [pinError, setPinError] = useState(null); // Effect that is triggered when the `authToken` comes back from the Plex OAuth @@ -69,6 +72,9 @@ const Login = () => { case 'REQUIRES_PIN': { setPinProfileId(response.data.profileId); setPinProfileName(response.data.profileName); + setPinProfileThumb(response.data.profileThumb); + setPinIsProtected(response.data.isProtected); + setPinIsMainUser(response.data.isMainUser); setShowPinEntry(true); break; } @@ -131,12 +137,32 @@ const Login = () => { if (response.data?.status === 'REQUIRES_PIN') { setShowPinEntry(true); + setPinProfileId(profileId); + setPinProfileName( + profiles.find((p) => p.id === profileId)?.title || + profiles.find((p) => p.id === profileId)?.username || + 'Profile' + ); + setPinProfileThumb( + profiles.find((p) => p.id === profileId)?.thumb || null + ); + setPinIsProtected( + profiles.find((p) => p.id === profileId)?.protected || false + ); + setPinIsMainUser( + profiles.find((p) => p.id === profileId)?.isMainUser || false + ); setPinError(intl.formatMessage(messages.invalidPin)); throw new Error('Invalid PIN'); } else { setShowProfileSelector(false); setShowPinEntry(false); setPinError(null); + setPinProfileId(null); + setPinProfileName(null); + setPinProfileThumb(null); + setPinIsProtected(false); + setPinIsMainUser(false); revalidate(); } } catch (e) { @@ -301,6 +327,10 @@ const Login = () => { { return handleSubmitProfile(pinProfileId, pin); }} @@ -308,10 +338,12 @@ const Login = () => { setShowPinEntry(false); setPinProfileId(null); setPinProfileName(null); + setPinProfileThumb(null); + setPinIsProtected(false); + setPinIsMainUser(false); setPinError(null); setShowProfileSelector(true); }} - error={pinError} /> ) : showProfileSelector ? (