fix: credentials login error and all-day resize bar position

- Fix NextAuth CredentialsProvider + PrismaAdapter conflict by stubbing
  adapter session methods (JWT strategy doesn't use DB sessions)
- Improve login error handling with redirect:false for real error messages
- Move all-day section resize bar to top when allDayPosition is "below"
- Fix resize drag direction for top-positioned all-day handle

v1.51.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-19 09:02:26 +01:00
parent eeabe2c1c5
commit 918d87cbbb
4 changed files with 69 additions and 26 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.51.1", "version": "1.51.2",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -11,16 +11,35 @@ function LoginContent() {
const error = searchParams.get('error'); const error = searchParams.get('error');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [credError, setCredError] = useState<string | null>(null);
const handleCredentialsSignIn = async (e: React.FormEvent<HTMLFormElement>) => { const handleCredentialsSignIn = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setCredError(null);
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
await signIn('credentials', { try {
const result = await signIn('credentials', {
email: formData.get('email'), email: formData.get('email'),
password: formData.get('password'), password: formData.get('password'),
callbackUrl, redirect: false,
}); });
if (result?.error) {
if (result.error === 'CredentialsSignin') {
setCredError('Invalid email or password');
} else {
setCredError(result.error);
}
setIsLoading(false);
} else if (result?.ok) {
window.location.href = callbackUrl;
}
} catch {
setCredError('An unexpected error occurred');
setIsLoading(false);
}
}; };
const handleGoogleSignIn = () => { const handleGoogleSignIn = () => {
@ -42,11 +61,15 @@ function LoginContent() {
</p> </p>
{/* Error Message */} {/* Error Message */}
{error && ( {(error || credError) && (
<div className="weekly-auth-error"> <div className="weekly-auth-error">
{error === 'CredentialsSignin' {credError
? credError
: error === 'CredentialsSignin'
? 'Invalid email or password' ? 'Invalid email or password'
: 'An error occurred during sign in'} : error === 'email_not_verified'
? 'Please verify your email before signing in'
: `An error occurred during sign in (${error})`}
</div> </div>
)} )}

View File

@ -1653,22 +1653,22 @@ export default function WeeklyView() {
} }
return null; return null;
}); });
const resizingRef = useRef<{ target: 'someday' | 'allday'; startY: number; startHeight: number } | null>(null); const resizingRef = useRef<{ target: 'someday' | 'allday'; startY: number; startHeight: number; handleOnTop: boolean } | null>(null);
const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday') => { const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday', handleOnTop = false) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
const section = target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement); const section = target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
if (!section) return; if (!section) return;
resizingRef.current = { target, startY: clientY, startHeight: section.getBoundingClientRect().height }; resizingRef.current = { target, startY: clientY, startHeight: section.getBoundingClientRect().height, handleOnTop };
const onMove = (ev: MouseEvent | TouchEvent) => { const onMove = (ev: MouseEvent | TouchEvent) => {
if (!resizingRef.current) return; if (!resizingRef.current) return;
const y = 'touches' in ev ? ev.touches[0].clientY : ev.clientY; const y = 'touches' in ev ? ev.touches[0].clientY : ev.clientY;
const rawDelta = y - resizingRef.current.startY; const rawDelta = y - resizingRef.current.startY;
// Top handle: dragging up = increase height (invert delta); bottom handle: normal // Top handle: dragging up = increase height (invert delta); bottom handle: normal
const delta = resizingRef.current.target === 'someday' ? -rawDelta : rawDelta; const delta = resizingRef.current.handleOnTop ? -rawDelta : rawDelta;
const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta)); const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta));
if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight); if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight);
else setAllDayHeight(newHeight); else setAllDayHeight(newHeight);
@ -5411,8 +5411,20 @@ export default function WeeklyView() {
); );
if (allDayEvents.length === 0) return null; if (allDayEvents.length === 0) return null;
const handleOnTop = effectiveAllDayPosition === "below";
const resizeHandle = isAllDayExpanded ? (
<div
className="resize-handle"
onMouseDown={(e) => startResize(e, 'allday', handleOnTop)}
onTouchStart={(e) => startResize(e, 'allday', handleOnTop)}
>
<div className="resize-handle-bar" />
</div>
) : null;
return ( return (
<> <>
{effectiveAllDayPosition === "below" && resizeHandle}
<section <section
className={`all-day-events-section ${isAllDayExpanded ? "expanded" : "collapsed"}`} className={`all-day-events-section ${isAllDayExpanded ? "expanded" : "collapsed"}`}
style={isAllDayExpanded && allDayHeight ? { height: `${allDayHeight}px`, overflowY: 'auto' } : undefined} style={isAllDayExpanded && allDayHeight ? { height: `${allDayHeight}px`, overflowY: 'auto' } : undefined}
@ -5532,16 +5544,7 @@ export default function WeeklyView() {
)} )}
</div> </div>
</section> </section>
{/* Resize handle - outside section so it's always at the bottom border */} {effectiveAllDayPosition === "above" && resizeHandle}
{isAllDayExpanded && (
<div
className="resize-handle"
onMouseDown={(e) => startResize(e, 'allday')}
onTouchStart={(e) => startResize(e, 'allday')}
>
<div className="resize-handle-bar" />
</div>
)}
</> </>
); );
})(); })();
@ -7315,8 +7318,8 @@ export default function WeeklyView() {
{somedayExpanded && ( {somedayExpanded && (
<div <div
className="resize-handle" className="resize-handle"
onMouseDown={(e) => startResize(e, 'someday')} onMouseDown={(e) => startResize(e, 'someday', true)}
onTouchStart={(e) => startResize(e, 'someday')} onTouchStart={(e) => startResize(e, 'someday', true)}
> >
<div className="resize-handle-bar" /> <div className="resize-handle-bar" />
</div> </div>

View File

@ -1,4 +1,5 @@
import { NextAuthOptions } from "next-auth"; import { NextAuthOptions } from "next-auth";
import type { Adapter } from "next-auth/adapters";
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from "next-auth/providers/google";
import AzureADProvider from "next-auth/providers/azure-ad"; import AzureADProvider from "next-auth/providers/azure-ad";
import AppleProvider from "next-auth/providers/apple"; import AppleProvider from "next-auth/providers/apple";
@ -7,8 +8,24 @@ import CredentialsProvider from "next-auth/providers/credentials";
import { compare } from "bcryptjs"; import { compare } from "bcryptjs";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
// Wrap PrismaAdapter to prevent session creation for credentials provider
// (known NextAuth v4 issue: adapter tries to create DB session even with JWT strategy)
const prismaAdapter = PrismaAdapter(prisma) as Adapter;
const adapter: Adapter = {
...prismaAdapter,
createUser: prismaAdapter.createUser,
getUser: prismaAdapter.getUser,
getUserByEmail: prismaAdapter.getUserByEmail,
getUserByAccount: prismaAdapter.getUserByAccount,
linkAccount: prismaAdapter.linkAccount,
createSession: () => { return null as any; },
getSessionAndUser: () => { return null as any; },
updateSession: () => { return null as any; },
deleteSession: () => { return null as any; },
};
export const authOptions: NextAuthOptions = { export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma), adapter,
providers: [ providers: [
GoogleProvider({ GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "", clientId: process.env.GOOGLE_CLIENT_ID || "",