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 @@
## 2025-05-22 - [Robust Error Synchronization in Login]
**Learning:** When syncing external error props to local state (e.g., in a LoginView), a simple string comparison in `useEffect` fails if the same error is triggered twice after being cleared locally. Using a 'derived state' pattern with a reference tracker or passing errors as unique objects from the parent ensures that the UI can reliably re-trigger error states.
**Action:** Use an object-based error state in the parent (e.g., `{ message: string } | null`) to ensure new references on every error event, and use `useRef` as a tracker in the child component to detect when a new error object has arrived.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*

node_modules
pnpm-lock.yaml

Choose a reason for hiding this comment

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

P2 Badge Stop ignoring pnpm lockfile

Tracking pnpm-lock.yaml is required for reproducible installs with pnpm; adding it to .gitignore means every install can resolve different dependency versions from the semver ranges in package.json, which can cause non-deterministic build/test failures across machines and CI. Please remove this ignore rule so lockfile updates are committed with dependency changes.

Useful? React with 👍 / 👎.

dist
dist-ssr
*.local
Expand Down
8 changes: 6 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const App: React.FC = () => {
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState<{ message: string } | null>(null);
const [isFaqGenerating, setIsFaqGenerating] = useState(false);
const [generatingStatus, setGeneratingStatus] = useState('');
const [isSyncing, setIsSyncing] = useState(false);
Expand Down Expand Up @@ -100,8 +101,11 @@ const App: React.FC = () => {
const user = users.find(u => u.email.toLowerCase() === email.toLowerCase() && u.status === 'aktiv');
if (user) {
setCurrentUser(user);
setLoginError(null);
} else {
alert("Zugang verweigert. Nur verifizierte Wohnpro-Bewohner können sich im Wohnpro Guide anmelden.");
// Using an object to ensure a new reference even if the message is the same,
// which triggers a re-render and allows LoginView to re-show the error.
setLoginError({ message: "Zugang verweigert. Nur verifizierte Wohnpro-Bewohner können sich im Wohnpro Guide anmelden." });
}
};

Expand Down Expand Up @@ -256,7 +260,7 @@ const App: React.FC = () => {
Synchronisiere mit Nextcloud...
</div>
)}
<LoginView onLogin={handleLogin} />
<LoginView onLogin={handleLogin} error={loginError} />
</>
);
}
Expand Down
6 changes: 6 additions & 0 deletions components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ const ChatView: React.FC<ChatViewProps> = ({ messages, onSendMessage, onEnterVoi
<form onSubmit={handleSubmit} className="relative group">
<div className="absolute inset-0 bg-black/5 blur-3xl rounded-full opacity-0 group-focus-within:opacity-100 transition-all duration-1000" />
<div className="relative flex items-center">
<label htmlFor="chat-input" className="sr-only">
Deine Nachricht an den Wohnpro Guide
</label>
<input
id="chat-input"
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
Expand All @@ -164,12 +168,14 @@ const ChatView: React.FC<ChatViewProps> = ({ messages, onSendMessage, onEnterVoi
onClick={onEnterVoice}
className="p-3.5 text-gray-400 hover:text-green-600 transition-all rounded-full hover:bg-gray-50 active:scale-90"
title="Sprachmodus"
aria-label="Sprachmodus starten"
>
<MicIcon className="w-7 h-7" />
</button>
<button
type="submit"
disabled={!input.trim() || isLoading}
aria-label="Nachricht senden"
className={`p-4 rounded-full transition-all active:scale-90 ${
input.trim() && !isLoading
? 'bg-black text-white shadow-xl shadow-black/20 hover:bg-gray-900'
Expand Down
38 changes: 29 additions & 9 deletions components/LoginView.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@

import React, { useState } from 'react';
import { User } from '../types';
import React, { useState, useEffect, useRef } from 'react';

interface LoginViewProps {
onLogin: (email: string) => void;
error?: string;
error?: { message: string } | null;
}

const LoginView: React.FC<LoginViewProps> = ({ onLogin, error: externalError }) => {
const [email, setEmail] = useState('');
const [error, setError] = useState(externalError || '');
const [localError, setLocalError] = useState('');
const prevExternalErrorRef = useRef<{ message: string } | null | undefined>(externalError);

// Sync external error to local state using tracker pattern
useEffect(() => {
if (externalError && externalError !== prevExternalErrorRef.current) {
setLocalError(externalError.message);
}
prevExternalErrorRef.current = externalError;
}, [externalError]);

const error = localError;

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!email) {
setError('Bitte gib deine Wohnpro E-Mail Adresse ein.');
setLocalError('Bitte gib deine Wohnpro E-Mail Adresse ein.');
return;
}
onLogin(email);
};

return (
<div className="fixed inset-0 bg-white flex items-center justify-center p-6 animate-in fade-in duration-700">
<div className="fixed inset-0 bg-white flex items-center justify-center p-6 animate-in fade-in duration-700" role="main">
<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">
Expand All @@ -35,15 +45,25 @@ 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">
Wohnpro E-Mail Adresse
</label>
<input
id="email-input"
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); setError(''); }}
onChange={(e) => { setEmail(e.target.value); setLocalError(''); }}
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" role="alert" className="mt-2 text-sm text-red-500 px-2 animate-in fade-in slide-in-from-top-1 duration-300">
{error}
</p>
)}
</div>

<button
Expand Down