Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-22 - [Accessible Login Error Handling]
**Learning:** Replacing browser `alert()` with inline UI error messages significantly improves UX by not blocking the UI thread. To ensure accessibility, use `aria-invalid`, `aria-describedby`, and `role="alert"`.
**Action:** When syncing external error props to local state, use a tracker variable (e.g., `prevExternalError`) to allow the error to be cleared locally and then re-triggered even if the parent passes the same error string again.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
pnpm-lock.yaml
preview.log

# Editor directories and files
.vscode/*
Expand Down
6 changes: 4 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const App: React.FC = () => {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [currentView, setCurrentView] = useState<View>('chat');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [loginError, setLoginError] = useState<string>('');

const [documents, setDocuments] = useState<Document[]>(() => {
const saved = localStorage.getItem(STORAGE_KEY);
Expand Down Expand Up @@ -96,12 +97,13 @@ const App: React.FC = () => {
}, [personas]);

const handleLogin = (email: string) => {
setLoginError('');
// If we have users loaded (from NC or Init), check them
const user = users.find(u => u.email.toLowerCase() === email.toLowerCase() && u.status === 'aktiv');
if (user) {
setCurrentUser(user);
} else {
alert("Zugang verweigert. Nur verifizierte Wohnpro-Bewohner können sich im Wohnpro Guide anmelden.");
setLoginError("Zugang verweigert. Nur verifizierte Wohnpro-Bewohner können sich im Wohnpro Guide anmelden.");
}
};

Expand Down Expand Up @@ -256,7 +258,7 @@ const App: React.FC = () => {
Synchronisiere mit Nextcloud...
</div>
)}
<LoginView onLogin={handleLogin} />
<LoginView onLogin={handleLogin} error={loginError} />
</>
);
}
Expand Down
17 changes: 14 additions & 3 deletions components/LoginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ interface LoginViewProps {
const LoginView: React.FC<LoginViewProps> = ({ onLogin, error: externalError }) => {
const [email, setEmail] = useState('');
const [error, setError] = useState(externalError || '');
const [prevExternalError, setPrevExternalError] = useState(externalError);

if (externalError !== prevExternalError) {
setError(externalError || '');
setPrevExternalError(externalError);
Comment on lines +15 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move prop-to-state sync out of render

Updating state during render (setError/setPrevExternalError) is not supported in React and can cause repeated renders or warnings in Strict/Concurrent mode when externalError changes. This sync should happen in a useEffect watching externalError; otherwise every prop change triggers a render-time state update which can lead to unstable behavior under React 18’s double-invocation rendering.

Useful? React with 👍 / 👎.

}

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!email) {
setError('Bitte gib deine Wohnpro E-Mail Adresse ein.');
return;
}
setPrevExternalError(undefined); // Reset to ensure the next error sync triggers
onLogin(email);
};

Expand All @@ -25,7 +32,7 @@ const LoginView: React.FC<LoginViewProps> = ({ onLogin, error: externalError })
<div className="w-full max-w-sm text-center">
<div className="mb-12">
<div className="w-16 h-16 bg-black rounded-3xl mx-auto flex items-center justify-center mb-6 shadow-xl shadow-black/10">
<svg className="w-8 h-8 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<svg aria-hidden="true" className="w-8 h-8 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
Expand All @@ -35,15 +42,19 @@ const LoginView: React.FC<LoginViewProps> = ({ onLogin, error: externalError })
</div>

<form onSubmit={handleSubmit} className="space-y-4">
<div className="relative group">
<div className="relative group text-left">
<label htmlFor="email-input" className="sr-only">E-Mail Adresse</label>
<input
id="email-input"
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); setError(''); }}
placeholder="Wohnpro E-Mail Adresse"
aria-invalid={!!error}
aria-describedby={error ? "login-error" : undefined}
className={`w-full bg-gray-50 border ${error ? 'border-red-200' : 'border-gray-100'} rounded-2xl px-6 py-4 focus:outline-none focus:ring-2 focus:ring-black/5 transition-all text-lg placeholder:text-gray-300`}
/>
{error && <p className="mt-2 text-sm text-red-500 text-left px-2">{error}</p>}
{error && <p id="login-error" className="mt-2 text-sm text-red-500 px-2" role="alert">{error}</p>}
</div>

<button
Expand Down