feat: dark mode color adjustment, multi-source quotes, UI text wrapping, and versioning rules

- Add dark mode color adjustment helper for category colors
- Support multiple motivational quote source URLs
- Fix text wrapping for tasks/events to prevent horizontal overflow
- Improve someday list item layout for long text
- Add CLAUDE.md with semantic versioning and git workflow rules
- Update test/doc URLs and planning docs

v1.1.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-02-23 23:33:25 +01:00
parent 8496c2daa4
commit c82572935c
27 changed files with 394 additions and 218 deletions

View File

@ -127,7 +127,7 @@ We need to implement the full authentication logic and ensure proper session man
- Set cookie with security flags (httpOnly, secure, sameSite)
- Use jose library instead of jsonwebtoken to avoid CommonJS issues with Edge runtime
</action>
<verify>Run `curl -X POST http://localhost:3000/api/auth/signup -H "Content-Type: application/json" -d '{"email":"test@example.com","password":"password"}'` and verify response structure</verify>
<verify>Run `curl -X POST http://localhost:3001/api/auth/signup -H "Content-Type: application/json" -d '{"email":"test@example.com","password":"password"}'` and verify response structure</verify>
<done>User can successfully sign up and receive a valid session token in HTTP-only cookie</done>
</task>
@ -151,7 +151,7 @@ We need to implement the full authentication logic and ensure proper session man
- Use jose library instead of jsonwebtoken to avoid CommonJS issues with Edge runtime
- Prevent timing attacks by comparing passwords in constant time
</action>
<verify>Run `curl -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d '{"email":"test@example.com","password":"password"}'` and verify response structure</verify>
<verify>Run `curl -X POST http://localhost:3001/api/auth/login -H "Content-Type: application/json" -d '{"email":"test@example.com","password":"password"}'` and verify response structure</verify>
<done>User can successfully authenticate and receive a valid session token in HTTP-only cookie</done>
</task>
@ -164,7 +164,7 @@ We need to implement the full authentication logic and ensure proper session man
- Return success message to the client
- Ensure cookie flags match those used during login (httpOnly, secure, sameSite)
</action>
<verify>Run `curl -X POST http://localhost:3000/api/auth/logout` and verify cookie is cleared</verify>
<verify>Run `curl -X POST http://localhost:3001/api/auth/logout` and verify cookie is cleared</verify>
<done>User can successfully log out and session cookie is cleared</done>
</task>

View File

@ -102,7 +102,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Send email using configured email service (details in infrastructure)
- Not reveal if email exists in system to prevent enumeration
</action>
<verify>Run `curl -X POST http://localhost:3000/api/auth/forgot-password -H "Content-Type: application/json" -d '{"email":"test@example.com"}'` and verify behavior</verify>
<verify>Run `curl -X POST http://localhost:3001/api/auth/forgot-password -H "Content-Type: application/json" -d '{"email":"test@example.com"}'` and verify behavior</verify>
<done>User can request password reset and receive confirmation without revealing account existence</done>
</task>
@ -125,7 +125,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Return appropriate error responses for invalid or expired tokens
- Use bcryptjs for password hashing
</action>
<verify>Run `curl -X POST http://localhost:3000/api/auth/reset-password -H "Content-Type: application/json" -d '{"email":"test@example.com","token":"reset-token","password":"newpassword"}'` and verify behavior</verify>
<verify>Run `curl -X POST http://localhost:3001/api/auth/reset-password -H "Content-Type: application/json" -d '{"email":"test@example.com","token":"reset-token","password":"newpassword"}'` and verify behavior</verify>
<done>User can complete password reset with valid token and receive confirmation</done>
</task>
@ -148,7 +148,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Clear token from database after verification
- Handle invalid/missing/expired tokens gracefully
</action>
<verify>Visit `http://localhost:3000/api/auth/verify-email?token=verification-token` and verify behavior</verify>
<verify>Visit `http://localhost:3001/api/auth/verify-email?token=verification-token` and verify behavior</verify>
<done>User can verify email address with valid token and receive confirmation</done>
</task>
@ -169,7 +169,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Be styled consistently with the rest of the application
- Have a link back to login page
</action>
<verify>Visit http://localhost:3000/auth/forgot-password and verify form renders correctly</verify>
<verify>Visit http://localhost:3001/auth/forgot-password and verify form renders correctly</verify>
<done>Forgot password page is accessible, visually consistent, and functional</done>
</task>
@ -192,7 +192,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Be styled consistently with the rest of the application
- Have a link back to login page
</action>
<verify>Visit http://localhost:3000/auth/reset-password?token=test-token and verify form renders correctly</verify>
<verify>Visit http://localhost:3001/auth/reset-password?token=test-token and verify form renders correctly</verify>
<done>Password reset page is accessible, visually consistent, and functional</done>
</task>
@ -213,7 +213,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Provide clear feedback on verification status
- Be styled consistently with the rest of the application
</action>
<verify>Visit http://localhost:3000/auth/verify-email?token=test-token and verify behavior</verify>
<verify>Visit http://localhost:3001/auth/verify-email?token=test-token and verify behavior</verify>
<done>Email verification page is accessible, visually consistent, and functional</done>
</task>

View File

@ -88,7 +88,7 @@ The previous plans have established core authentication flows. This plan focuses
- Send email using configured email service (details in infrastructure)
- Return clear success/failure messages
</action>
<verify>Run `curl -X POST http://localhost:3000/api/auth/send-verification-email -H "Content-Type: application/json" -d '{"email":"test@example.com"}'` and verify behavior</verify>
<verify>Run `curl -X POST http://localhost:3001/api/auth/send-verification-email -H "Content-Type: application/json" -d '{"email":"test@example.com"}'` and verify behavior</verify>
<done>User can request verification email to be resent and receive confirmation</done>
</task>

View File

@ -85,7 +85,7 @@ Output: Bidirectional synchronization logic, sync API endpoint, and calendar set
- Handles errors gracefully with appropriate messaging
Follow the API endpoint pattern established in Phase 2 and authentication flow from Phase 1.</action>
<verify>curl -X POST http://localhost:3000/api/calendar/sync returns 200 with successful sync status</verify>
<verify>curl -X POST http://localhost:3001/api/calendar/sync returns 200 with successful sync status</verify>
<done>Endpoint successfully triggers sync process and returns status JSON</done>
</task>

View File

@ -80,7 +80,7 @@ Functional OAuth endpoints that exchange authorization codes for real tokens and
- Handle and log token exchange errors appropriately
- Store the tokens in the CalendarConnection database record
</action>
<verify>Run `curl -X GET "http://localhost:3000/api/calendar/google/oauth?code=valid_code"` and verify tokens are stored in database instead of just console logged</verify>
<verify>Run `curl -X GET "http://localhost:3001/api/calendar/google/oauth?code=valid_code"` and verify tokens are stored in database instead of just console logged</verify>
<done>Google OAuth callback endpoint exchanges real tokens and stores them in database</done>
</task>
@ -102,7 +102,7 @@ Functional OAuth endpoints that exchange authorization codes for real tokens and
- Handle and log token exchange errors appropriately
- Store the tokens in the CalendarConnection database record
</action>
<verify>Run `curl -X GET "http://localhost:3000/api/calendar/apple/oauth?code=valid_code"` and verify tokens are stored in database instead of just console logged</verify>
<verify>Run `curl -X GET "http://localhost:3001/api/calendar/apple/oauth?code=valid_code"` and verify tokens are stored in database instead of just console logged</verify>
<done>Apple OAuth callback endpoint exchanges real tokens and stores them in database</done>
</task>

52
CLAUDE.md Normal file
View File

@ -0,0 +1,52 @@
# Project Instructions for Claude Code
## Project Overview
My Weekly ToDo List - A web-based weekly task management application built with Next.js.
Deployed at: todo.martin-bierschenk.de
## Git Workflow & Versioning
### Branch Strategy (Git Flow Lite)
- `main` - stable, deployed to todo.martin-bierschenk.de
- `dev` - working branch for new features
- Feature branches off `dev` for larger work
- Merge `dev``main` when ready to deploy
- `main` always reflects what's live
### Semantic Versioning (REQUIRED for every commit)
Use semver (MAJOR.MINOR.PATCH) in `package.json`:
**Version bump rules:**
- **PATCH** (x.x.+1): Bug fixes, typo corrections, minor CSS tweaks, dependency updates
- Commit prefix: `fix:`, `chore:`, `style:`, `docs:`
- **MINOR** (x.+1.0): New features, significant UI changes, new integrations
- Commit prefix: `feat:`
- **MAJOR** (+1.0.0): Breaking changes (database migrations, API changes, auth changes)
- Commit prefix: any with `BREAKING CHANGE` in body
**On every git commit, you MUST:**
1. Determine the version bump type based on changes (patch/minor/major)
2. Update the `version` field in `package.json`
3. Include the new version in the commit message footer, e.g.: `v1.1.0`
4. After committing, create a git tag: `git tag v<version>`
**Commit message format:**
```
<type>: <description>
<optional body>
v<new-version>
```
Example:
```
feat: add Microsoft To-Do integration
Sync tasks bidirectionally with Microsoft To-Do API.
v1.1.0
```
### Current Version
Check `package.json``version` field for the current version before bumping.

View File

@ -47,6 +47,6 @@ USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/api/auth/session || exit 1
CMD curl -f http://localhost:3001/api/auth/session || exit 1
CMD ["npm", "run", "start"]

View File

@ -26,7 +26,7 @@ This application can be deployed using Docker and Docker Compose.
```bash
docker-compose up --build
```
3. Access the application at http://localhost:3000
3. Access the application at http://localhost:3001
### Environment Variables

View File

@ -39,7 +39,7 @@ This will create all necessary tables:
npm run dev
```
Visit: **http://localhost:3000**
Visit: **http://localhost:3001**
---
@ -47,14 +47,14 @@ Visit: **http://localhost:3000**
### Sign Up (Email/Password)
1. Go to http://localhost:3000/auth/signup
1. Go to http://localhost:3001/auth/signup
2. Enter email and password (min 8 characters)
3. Click "Sign up"
4. You'll be automatically signed in
### Sign In (Email/Password)
1. Go to http://localhost:3000/auth/login
1. Go to http://localhost:3001/auth/login
2. Enter your credentials
3. Click "Sign in"
@ -82,7 +82,7 @@ To enable "Sign in with Google":
4. Click **Create Credentials** → **OAuth client ID**
5. Choose **Web application**
6. Add these redirect URIs:
- `http://localhost:3000/api/auth/callback/google`
- `http://localhost:3001/api/auth/callback/google`
- `https://todo.martin-bierschenk.de/api/auth/callback/google` (via Reverse Proxy)
- `https://todo.martin-bierschenk.de/api/calendar/google/oauth` (via Reverse Proxy)
7. Copy your **Client ID** and **Client Secret**

View File

@ -161,7 +161,7 @@ The application follows a standard Next.js 14 architecture with:
## Assumptions
1. Application runs on localhost:3000 (development server)
1. Application runs on localhost:3001 (development server)
2. All components are properly rendered
3. Network connectivity allows for API calls
4. Required environment variables are configured

View File

@ -41,7 +41,7 @@ I've created a comprehensive test plan for the My Weekly To Do List application
## Implementation Notes
The test plan assumes:
- Application will run on localhost:3000 during testing
- Application will run on localhost:3001 during testing
- All components are properly implemented and accessible
- Environment variables are configured appropriately
- Network connectivity supports API interactions

View File

@ -1,9 +1,9 @@
// Debug script to inspect login page structure and Tailwind CSS
// Run this by visiting http://localhost:3000/auth/login in a browser and using DevTools
// Run this by visiting http://localhost:3001/auth/login in a browser and using DevTools
console.log('=== DEBUGGING LOGIN PAGE STRUCTURE ===');
// This will be run in browser console after navigating to http://localhost:3000/auth/login
// This will be run in browser console after navigating to http://localhost:3001/auth/login
// 1. Check if page loads properly
console.log('1. Checking page load...');
@ -23,7 +23,7 @@ console.log('\n3. Checking login elements:');
const h1Elements = document.querySelectorAll('h1');
console.log('H1 elements count:', h1Elements.length);
h1Elements.forEach((el, i) => {
console.log(` H1 ${i+1}: "${el.textContent}"`);
console.log(` H1 ${i + 1}: "${el.textContent}"`);
});
// 4. Check for form elements
@ -59,7 +59,7 @@ const inputs = document.querySelectorAll('input');
console.log('Inputs found:', inputs.length);
inputs.forEach((input, i) => {
if (input.type === 'email' || input.type === 'password') {
console.log(` Input ${i+1}: type=${input.type}, name=${input.name}`);
console.log(` Input ${i + 1}: type=${input.type}, name=${input.name}`);
console.log(` Classes: ${input.className}`);
}
});
@ -67,7 +67,7 @@ inputs.forEach((input, i) => {
const buttons = document.querySelectorAll('button');
console.log('Buttons found:', buttons.length);
buttons.forEach((btn, i) => {
console.log(` Button ${i+1}: type=${btn.type}, text="${btn.textContent.substring(0, 30)}..."`);
console.log(` Button ${i + 1}: type=${btn.type}, text="${btn.textContent.substring(0, 30)}..."`);
});
// 8. Check for expected text content

View File

@ -4,7 +4,7 @@
* This script is meant to be run in the browser console to debug signup issues.
*
* To use:
* 1. Navigate to http://localhost:3000/auth/signup
* 1. Navigate to http://localhost:3001/auth/signup
* 2. Open browser developer tools (F12)
* 3. Go to Console tab
* 4. Paste this entire script and run it
@ -30,7 +30,7 @@ if (!signupForm) {
});
} else {
console.log('✅ Form found:', signupForm);
// Check form attributes
console.log('Form attributes:', {
action: signupForm.action,
@ -38,28 +38,28 @@ if (!signupForm) {
id: signupForm.id,
className: signupForm.className
});
// Add temporary event listener to see if submit is being called
signupForm.addEventListener('submit', function(e) {
signupForm.addEventListener('submit', function (e) {
console.log('📋 Form submit intercepted');
console.log('Submit event:', e);
console.log('Form data will be sent to:', this.action || 'default');
}, { once: true });
// Check input fields
const emailInput = document.querySelector('input[name="email"]');
const passwordInput = document.querySelector('input[name="password"]');
const submitButton = document.querySelector('button[type="submit"]');
console.log('Input fields:');
if (emailInput) console.log('✅ Email input:', emailInput);
if (passwordInput) console.log('✅ Password input:', passwordInput);
if (submitButton) console.log('✅ Submit button:', submitButton);
// Monitor network requests
console.log('🔍 Monitoring network requests...');
const originalFetch = window.fetch;
window.fetch = function(...args) {
window.fetch = function (...args) {
console.log('📡 Fetch called with:', args);
const [input, init] = args;
console.log(' URL:', input);
@ -69,9 +69,9 @@ if (!signupForm) {
}
return originalFetch.apply(this, args);
};
// Monitor for errors
window.addEventListener('error', function(e) {
window.addEventListener('error', function (e) {
console.error('🚨 JS Error caught:', e.error);
console.error('Error location:', e.filename, e.lineno, e.colno);
});

View File

@ -41,7 +41,7 @@ Since this is a Next.js application, you can deploy it with:
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD curl -f http://localhost:3001/health || exit 1
# For Next.js, run in production mode
CMD ["npm", "run", "start"]

View File

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

View File

@ -26,7 +26,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: 'http://localhost:3000',
baseURL: 'http://localhost:3001',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
@ -73,7 +73,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// url: 'http://localhost:3001',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@ -465,6 +465,8 @@ h3 {
--weekly-settings-toggle-active-bg: white;
--weekly-settings-toggle-text: #6b7280;
--weekly-settings-toggle-active-text: #111827;
--weekly-header-brightness: 1;
--weekly-goal-brightness: 1;
}
/* Weekly Main Container */
@ -709,7 +711,9 @@ h3 {
position: relative;
transition: all 0.2s ease;
border-bottom: 1px solid var(--weekly-border); /* Restore lines */
line-height: initial;
line-height: 1.4;
height: auto;
min-height: 32px;
}
/* Remove border from last item to look cleaner, or keep for paper look */
@ -737,6 +741,9 @@ h3 {
font-weight: var(--weekly-task-weight, 400);
line-height: var(--weekly-task-line-height, 1.5);
color: var(--weekly-task-color, var(--weekly-text));
white-space: normal;
word-break: break-word;
overflow: visible;
border: none;
outline: none;
background: transparent;
@ -1264,19 +1271,20 @@ h3 {
.weekly-someday-list .weekly-task-item {
border-bottom: 1px solid transparent;
height: 32px;
padding: 0 1rem;
min-height: 32px;
height: auto;
padding: 4px 1rem;
display: flex;
align-items: center;
align-items: flex-start;
}
.weekly-someday-list .weekly-task-text {
font-size: var(--base-font-size);
line-height: 32px;
max-height: 32px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.4;
word-break: break-word;
overflow: visible;
white-space: normal;
text-overflow: clip;
padding: 0;
}
@ -1607,6 +1615,8 @@ h3 {
--weekly-settings-text: #ffffff;
--weekly-settings-toggle-bg: #222222;
--weekly-settings-toggle-active-bg: #333333;
--weekly-header-brightness: 1.5;
--weekly-goal-brightness: 1.5;
--weekly-settings-toggle-text: #888888;
--weekly-settings-toggle-active-text: #ffffff;
}

View File

@ -39,6 +39,7 @@ import {
Palette,
Sparkles,
Info,
Trash2,
} from "lucide-react";
// Types
@ -418,6 +419,29 @@ function invertColor(hex: string): string {
}
}
// Helper to lighten color for dark mode
function adjustColorForDarkMode(hex: string, isDarkMode: boolean): string {
if (!isDarkMode || !hex || !hex.startsWith("#")) return hex;
// Simple hex to RGB
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
// Calculate brightness (0-255)
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// If it's too dark for dark mode, lighten it
if (brightness < 120) {
r = Math.min(255, r + 100);
g = Math.min(255, g + 100);
b = Math.min(255, b + 100);
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
}
return hex;
}
// Main Component
export default function WeeklyView() {
const { data: session } = useSession();
@ -558,6 +582,7 @@ export default function WeeklyView() {
yearFontSize?: string;
yearFontWeight?: string;
yearColor?: string;
quoteSourceUrls?: string[];
}>({
name: session?.user?.name || "",
email: session?.user?.email || "",
@ -609,6 +634,7 @@ export default function WeeklyView() {
weekendColorSun: "#dc2626",
pastDayColor: "#a6a6a7",
quoteSourceUrl: "https://recite.vercel.app/api/random",
quoteSourceUrls: ["https://recite.vercel.app/api/random"],
});
const [motivationalQuote, setMotivationalQuote] = useState("");
const [showSummary, setShowSummary] = useState(false);
@ -945,31 +971,42 @@ export default function WeeklyView() {
const fetchMotivationalQuote = useCallback(async () => {
if (profile.goalFallbackType !== "quote") return;
try {
const url = profile.quoteSourceUrl || "https://recite.vercel.app/api/random";
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch quote");
const data = await res.json();
// Handle different JSON formats (array or object)
let quoteText = "";
if (Array.isArray(data) && data.length > 0) {
const item = data[0];
quoteText = item.quote || item.text || item.content || (typeof item === 'string' ? item : "");
if (item.author) quoteText += ` - ${item.author}`;
} else if (data && typeof data === 'object') {
quoteText = data.quote || data.text || data.content || "";
if (data.author) quoteText += ` - ${data.author}`;
} else if (typeof data === 'string') {
quoteText = data;
const urls = profile.quoteSourceUrls && profile.quoteSourceUrls.length > 0
? profile.quoteSourceUrls
: [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"];
// Strategy: try sources until one works
for (const url of urls) {
try {
const res = await fetch(url);
if (!res.ok) continue;
const data = await res.json();
let quoteText = "";
if (Array.isArray(data) && data.length > 0) {
const item = data[0];
quoteText = item.quote || item.text || item.content || (typeof item === 'string' ? item : "");
if (item.author) quoteText += ` - ${item.author}`;
} else if (data && typeof data === 'object') {
quoteText = data.quote || data.text || data.content || "";
if (data.author) quoteText += ` - ${data.author}`;
} else if (typeof data === 'string') {
quoteText = data;
}
if (quoteText) {
setMotivationalQuote(quoteText);
return; // Success!
}
} catch (error) {
console.error(`Error fetching quote from ${url}:`, error);
}
if (quoteText) setMotivationalQuote(quoteText);
} catch (error) {
console.error("Error fetching motivational quote:", error);
setMotivationalQuote("Stay focused and productive.");
}
}, [profile.goalFallbackType, profile.quoteSourceUrl]);
// Final fallback if all failed
setMotivationalQuote("Stay focused and productive.");
}, [profile.goalFallbackType, profile.quoteSourceUrl, profile.quoteSourceUrls]);
// Fetch tasks on mount
useEffect(() => {
@ -1457,15 +1494,17 @@ export default function WeeklyView() {
const dateStr = formatDateToISO(date); // Use local date formatting
return tasks
.filter((task) => {
if (!task.scheduledDate) return false;
// Exclude sub-tasks from top-level list (they render inside their parent)
if (task.parentTaskId) return false;
// Use string comparison to avoid timezone shifts
const taskDateStr =
typeof task.scheduledDate === "string"
? task.scheduledDate.substring(0, 10)
: formatDateToISO(new Date(task.scheduledDate));
return taskDateStr === dateStr;
if (!task.completed && task.scheduledDate) {
// Exclude sub-tasks from top-level list (they render inside their parent)
if (task.parentTaskId) return false;
// Use string comparison to avoid timezone shifts
const taskDateStr =
typeof task.scheduledDate === "string"
? task.scheduledDate.substring(0, 10)
: formatDateToISO(new Date(task.scheduledDate));
return taskDateStr === dateStr;
}
return false;
})
.sort((a, b) => {
// Sort by time if available
@ -1798,6 +1837,9 @@ export default function WeeklyView() {
const taskDuration = task.duration || 15;
const taskEnd = taskStart + taskDuration;
// Skip completed tasks (they don't render in the grid)
if (task.completed) return false;
// Overlap condition: task starts before slot ends AND task ends after slot starts
return taskStart < slotEnd && taskEnd > slotStart;
});
@ -3270,27 +3312,39 @@ export default function WeeklyView() {
</div>
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */}
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2">
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group">
{/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2">
{(isLoading || isSyncing || syncStatus === "syncing") && (
{(isLoading || isSyncing || syncStatus === "syncing") ? (
<div className="weekly-spinner" title="Syncing..."></div>
) : (
<button
onClick={() => { fetchCalendarEvents(); fetchTasks(); }}
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100"
title="Refresh Calendar & Tasks"
>
<RefreshCcw size={14} />
</button>
)}
<span style={{
fontFamily: profile.cwFontFamily || "Inter",
fontSize: profile.cwFontSize || "1.125rem",
fontWeight: Number(profile.cwFontWeight || "700"),
color: profile.cwColor || "#333333",
color: adjustColorForDarkMode(profile.cwColor || "#333333", darkMode),
filter: "brightness(var(--weekly-header-brightness, 1))"
}}>
KW {getWeekNumber(currentWeekStart).toString().padStart(2, "0")}
</span>
<span className="text-gray-400">|</span>
<span style={{
fontFamily: profile.yearFontFamily || "Inter",
fontSize: profile.yearFontSize || "1.125rem",
fontWeight: Number(profile.yearFontWeight || "700"),
color: profile.yearColor || "#333333",
}}>
<span
style={{
fontFamily: profile.yearFontFamily || "Inter",
fontSize: profile.yearFontSize || "1.125rem",
fontWeight: Number(profile.yearFontWeight || "700"),
color: adjustColorForDarkMode(profile.yearColor || "#333333", darkMode),
filter: "brightness(var(--weekly-header-brightness, 1))"
}}
>
{currentWeekStart.getFullYear()}
</span>
</div>
@ -3330,6 +3384,8 @@ export default function WeeklyView() {
: undefined,
fontSize: profile.goalFontSize || undefined,
fontWeight: profile.goalFontWeight || undefined,
color: adjustColorForDarkMode((profile.goalFallbackType === "quote" ? profile.taskColor : undefined) || "#333333", darkMode),
filter: "brightness(var(--weekly-goal-brightness, 1))"
}}
>
{showNextTask
@ -5640,7 +5696,15 @@ function TaskItem({
</svg>
</span>
)}
<span className="truncate">{task.title}</span>
<span
style={{
whiteSpace: "normal",
wordBreak: "break-word",
overflow: "visible"
}}
>
{task.title}
</span>
</span>
<div className="task-actions absolute right-0 top-0 bottom-0 flex items-center bg-white/95 dark:bg-gray-800/95 pl-2 pr-1 shadow-[-12px_0_12px_-4px_rgba(255,255,255,0.95)] dark:shadow-[-12px_0_12px_-4px_rgba(31,41,55,0.95)] z-20">
@ -6062,7 +6126,9 @@ interface SettingsSidebarProps {
yearFontSize?: string;
yearFontWeight?: string;
yearColor?: string;
quoteSourceUrls: string[];
}) => void;
quoteSourceUrls?: string[];
goal: string;
setGoal: (goal: string) => void;
saveGoal: (goal: string) => void;
@ -6288,6 +6354,7 @@ function SettingsSidebar({
yearFontSize?: string;
yearFontWeight?: string;
yearColor?: string;
quoteSourceUrls?: string[];
}>({
name: "",
email: "",
@ -8995,7 +9062,7 @@ function SettingsSidebar({
flex: 1,
padding: "10px",
borderRadius: "6px",
border: "none",
border: profile.goalScope === "week" ? "2px solid var(--weekly-teal)" : "none",
background:
profile.goalScope === "week"
? "var(--weekly-settings-toggle-active-bg)"
@ -9004,7 +9071,8 @@ function SettingsSidebar({
profile.goalScope === "week"
? "var(--weekly-settings-toggle-active-text)"
: "var(--weekly-settings-text)",
fontWeight: 600,
fontWeight: 700,
boxShadow: profile.goalScope === "week" ? "0 2px 4px rgba(0,0,154,0.1)" : "none",
cursor: "pointer",
transition: "all 0.2s",
}}
@ -9020,7 +9088,7 @@ function SettingsSidebar({
flex: 1,
padding: "10px",
borderRadius: "6px",
border: "none",
border: profile.goalScope === "day" ? "2px solid var(--weekly-teal)" : "none",
background:
profile.goalScope === "day"
? "var(--weekly-settings-toggle-active-bg)"
@ -9029,7 +9097,8 @@ function SettingsSidebar({
profile.goalScope === "day"
? "var(--weekly-settings-toggle-active-text)"
: "var(--weekly-settings-text)",
fontWeight: 600,
fontWeight: 700,
boxShadow: profile.goalScope === "day" ? "0 2px 4px rgba(0,154,154,0.1)" : "none",
cursor: "pointer",
transition: "all 0.2s",
}}
@ -9104,28 +9173,73 @@ function SettingsSidebar({
color: "var(--weekly-settings-label)",
}}
>
API-Datenquelle (URL)
API-Datenquellen (URLs)
</label>
<input
type="text"
value={profile.quoteSourceUrl || ""}
onChange={(e) =>
setProfile((p) => ({
...p,
quoteSourceUrl: e.target.value,
}))
}
className="weekly-input"
placeholder="https://recite.vercel.app/api/random"
style={{
width: "100%",
padding: "10px",
fontSize: "0.95rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border)",
background: "var(--weekly-bg)",
}}
/>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{(profile.quoteSourceUrls || [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"]).map((url: string, idx: number) => (
<div key={idx} style={{ display: "flex", gap: "8px" }}>
<input
type="text"
value={url}
onChange={(e) => {
const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"])];
newUrls[idx] = e.target.value;
setProfile((p) => ({ ...p, quoteSourceUrls: newUrls }));
}}
className="weekly-input"
placeholder="https://..."
style={{
flex: 1,
padding: "10px",
fontSize: "0.95rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border)",
background: "var(--weekly-bg)",
}}
/>
<button
type="button"
onClick={() => {
const newUrls = (profile.quoteSourceUrls || [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"]).filter((_val: string, i: number) => i !== idx);
setProfile((p) => ({ ...p, quoteSourceUrls: newUrls }));
}}
style={{
padding: "8px",
background: "#fee2e2",
color: "#ef4444",
border: "none",
borderRadius: "6px",
cursor: "pointer"
}}
>
<Trash2 size={16} />
</button>
</div>
))}
<button
type="button"
onClick={() => {
const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"]), ""];
setProfile((p) => ({ ...p, quoteSourceUrls: newUrls }));
}}
style={{
alignSelf: "flex-start",
marginTop: "4px",
padding: "6px 12px",
fontSize: "0.85rem",
background: "var(--weekly-teal)",
color: "white",
border: "none",
borderRadius: "6px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "4px"
}}
>
<Plus size={14} /> Add Source
</button>
</div>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-text-light)", marginTop: "4px" }}>
URL that returns a JSON list or object of quotes (e.g. {'[{"quote":"...","author":"..."}]'} or {'{"quote":"..."}'}).
</p>

View File

@ -4,18 +4,18 @@
*
* Usage:
* 1. Start the development server: npm run dev
* 2. Visit http://localhost:3000/auth/signup in your browser
* 2. Visit http://localhost:3001/auth/signup in your browser
* 3. Fill in the form with:
* - Email: mail@martin-bierschenk.de
* - Password: My-Weekly-ToDo-List
* 4. Or use the following curl command:
*
* curl -X POST http://localhost:3000/api/auth/signup \
* curl -X POST http://localhost:3001/api/auth/signup \
* -H "Content-Type: application/json" \
* -d '{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}'
*
* For login after signup:
* curl -X POST http://localhost:3000/api/auth/login \
* curl -X POST http://localhost:3001/api/auth/login \
* -H "Content-Type: application/json" \
* -d '{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}'
*/
@ -32,18 +32,18 @@ console.log('Email: mail@martin-bierschenk.de');
console.log('Password: My-Weekly-ToDo-List');
console.log('');
console.log('Method 1: Using Browser Interface');
console.log('1. Navigate to http://localhost:3000/auth/signup');
console.log('1. Navigate to http://localhost:3001/auth/signup');
console.log('2. Fill in the form with the credentials above');
console.log('3. Submit the form');
console.log('');
console.log('Method 2: Using curl commands (in terminal)');
console.log('First, create the account:');
console.log('curl -X POST http://localhost:3000/api/auth/signup \\');
console.log('curl -X POST http://localhost:3001/api/auth/signup \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -d \'{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}\'');
console.log('');
console.log('Then, authenticate with the account:');
console.log('curl -X POST http://localhost:3000/api/auth/login \\');
console.log('curl -X POST http://localhost:3001/api/auth/login \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -d \'{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}\'');
console.log('');

View File

@ -5,9 +5,9 @@ const fetch = require('node-fetch');
async function testSignupApi() {
console.log('Testing signup API endpoint...');
try {
const response = await fetch('http://localhost:3000/api/auth/signup', {
const response = await fetch('http://localhost:3001/api/auth/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -17,13 +17,13 @@ async function testSignupApi() {
password: 'testpassword'
})
});
console.log('Status:', response.status);
console.log('Headers:', response.headers.raw());
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
console.log('JSON Response:', data);
@ -32,7 +32,7 @@ async function testSignupApi() {
console.log('Non-JSON Response (likely HTML):');
console.log(text.substring(0, 500) + '...');
}
} catch (error) {
console.error('Error testing API:', error);
}

View File

@ -3,11 +3,11 @@ import { test, expect } from '@playwright/test';
// Test authentication flow against your running application
test.describe('Authentication Flow Tests', () => {
test('should access login page', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Check that we're on the login page
await expect(page).toHaveTitle(/Login/);
// Check for login form elements
await expect(page.getByText('Login')).toBeVisible();
await expect(page.getByPlaceholder('Email')).toBeVisible();
@ -16,11 +16,11 @@ test.describe('Authentication Flow Tests', () => {
});
test('should access signup page', async ({ page }) => {
await page.goto('http://localhost:3000/auth/signup');
await page.goto('http://localhost:3001/auth/signup');
// Check that we're on the signup page
await expect(page).toHaveTitle(/Sign Up/);
// Check for signup form elements
await expect(page.getByText('Sign Up')).toBeVisible();
await expect(page.getByPlaceholder('Email')).toBeVisible();

View File

@ -3,16 +3,16 @@ import { test, expect } from '@playwright/test';
// Test to verify CSS styling is working in your application
test.describe('CSS Debugging Tests', () => {
test('should load with proper CSS styling', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.goto('http://localhost:3001');
// Verify main page elements have proper styling
const mainHeader = page.getByText('My Weekly To Do List');
await expect(mainHeader).toBeVisible();
// Check for CSS classes that indicate styling is applied
const taskItem = page.locator('.task-item');
await expect(taskItem).toBeVisible();
// Verify basic CSS properties are applied
const taskItemStyles = await taskItem.evaluate(element => {
const computedStyle = window.getComputedStyle(element);
@ -22,18 +22,18 @@ test.describe('CSS Debugging Tests', () => {
boxShadow: computedStyle.boxShadow
};
});
console.log('Task item CSS properties:', taskItemStyles);
expect(taskItemStyles.backgroundColor).toBeTruthy();
});
test('should have proper auth form styling', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Check that auth form has proper styling
const authForm = page.locator('.auth-form');
await expect(authForm).toBeVisible();
// Verify CSS properties on auth form
const formStyles = await authForm.evaluate(element => {
const computedStyle = window.getComputedStyle(element);
@ -43,7 +43,7 @@ test.describe('CSS Debugging Tests', () => {
boxShadow: computedStyle.boxShadow
};
});
console.log('Auth form CSS properties:', formStyles);
expect(formStyles.backgroundColor).toBeTruthy();
});

View File

@ -3,19 +3,19 @@ import { test, expect } from '@playwright/test';
// Simple test to verify the app is running and accessible
test('My Weekly To Do List should be accessible', async ({ page }) => {
// Navigate to the running application
await page.goto('http://localhost:3000');
await page.goto('http://localhost:3001');
// Check that the main elements are present
await expect(page).toHaveTitle(/My Weekly To Do List/);
// Check for main UI elements
await expect(page.getByText('My Weekly To Do List')).toBeVisible();
await expect(page.getByText('Weekly View')).toBeVisible();
});
test('Should be able to access tasks page', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.goto('http://localhost:3001');
// Try to navigate to tasks page
const tasksLink = page.getByText('Tasks');
await expect(tasksLink).toBeVisible();

View File

@ -2,50 +2,50 @@ import { test, expect } from '@playwright/test';
test.describe('Login Page Core Functionality', () => {
test('should load the login page with core elements', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Basic page verification
await expect(page).toHaveURL(/login/);
await expect(page).toHaveTitle(/Weekly To Do List/);
// Check that the page has essential structure
const h1Element = page.locator('h1');
await expect(h1Element).toBeVisible();
// Check for a login-related paragraph
const loginParagraph = page.getByText(/Sign in to your account|Welcome back!/);
await expect(loginParagraph).toBeVisible();
// Check for form elements
const emailInput = page.locator('input[name="email"]');
const passwordInput = page.locator('input[name="password"]');
const submitButton = page.locator('button[type="submit"]');
await expect(emailInput).toBeVisible();
await expect(passwordInput).toBeVisible();
await expect(submitButton).toBeVisible();
// Check for links
const signupLink = page.getByRole('link', { name: /Don't have an account|Create account/ });
const forgotPasswordLink = page.getByRole('link', { name: /Forgot your password/ });
await expect(signupLink).toBeVisible();
await expect(forgotPasswordLink).toBeVisible();
console.log('✅ Login page loaded successfully with core elements');
});
test('should have basic CSS structure', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Check that the page has basic styling structure
const body = page.locator('body');
await expect(body).toBeVisible();
// Check for main wrapper div that should have Tailwind classes
const mainWrapper = page.locator('[class*="min-h-screen"]');
await expect(mainWrapper).toBeVisible();
console.log('✅ Basic CSS structure verified');
});
});

View File

@ -2,37 +2,37 @@ import { test, expect } from '@playwright/test';
test.describe('Login Page Debug - Simplified', () => {
test('should load login page successfully', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Basic page load verification
await expect(page).toHaveURL(/login/);
await expect(page).toHaveTitle(/Weekly To Do List/);
// Check for key elements that should be present
await expect(page.locator('h1')).toContainText('Weekly To Do List');
await expect(page.locator('p')).toContainText('Sign in to your account');
// Check for form elements
await expect(page.locator('input[name="email"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toBeVisible();
// Check for navigation links
await expect(page.getByRole('link', { name: 'Don\'t have an account?' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Forgot your password?' })).toBeVisible();
});
test('should have Tailwind CSS classes in structure', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Check main container classes
const container = page.locator('div.min-h-screen.bg-gray-50');
await expect(container).toBeVisible();
// Check the form container
const formContainer = page.locator('div.bg-white.rounded-xl.shadow-lg.p-8');
await expect(formContainer).toBeVisible();
// Check for basic Tailwind classes that should be present
const bodyClass = await page.evaluate(() => document.body.className);
expect(bodyClass).toContain('min-h-screen');
@ -40,19 +40,19 @@ test.describe('Login Page Debug - Simplified', () => {
});
test('should have functional form elements', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Test that the form exists
const form = page.locator('form');
await expect(form).toBeVisible();
// Test that we can interact with inputs
const emailInput = page.locator('input[name="email"]');
const passwordInput = page.locator('input[name="password"]');
await expect(emailInput).toBeVisible();
await expect(passwordInput).toBeVisible();
// Test that submit button exists
const submitButton = page.locator('button[type="submit"]');
await expect(submitButton).toBeVisible();

View File

@ -2,50 +2,50 @@ import { test, expect } from '@playwright/test';
test.describe('Login Page Debug Test', () => {
test('should load the login page correctly', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Check page title and heading
await expect(page).toHaveTitle(/Weekly To Do List/);
await expect(page.locator('h1')).toContainText('Weekly To Do List');
await expect(page.locator('p:has-text("Sign in to your account")')).toBeVisible();
// Check for Tailwind CSS classes in main container
const mainContainer = page.locator('div.min-h-screen.bg-gray-50');
await expect(mainContainer).toBeVisible();
// Check for form elements
const form = page.locator('form');
await expect(form).toBeVisible();
// Check for email input field
const emailInput = page.locator('#email-address');
await expect(emailInput).toBeVisible();
await expect(emailInput).toHaveAttribute('type', 'email');
await expect(emailInput).toHaveAttribute('placeholder', 'Email address');
// Check for password input field
const passwordInput = page.locator('#password');
await expect(passwordInput).toBeVisible();
await expect(passwordInput).toHaveAttribute('type', 'password');
await expect(passwordInput).toHaveAttribute('placeholder', 'Password');
// Check for submit button
const submitButton = page.locator('button[type="submit"]');
await expect(submitButton).toBeVisible();
await expect(submitButton).toContainText('Sign in');
// Check for links
await expect(page.locator('a:has-text("Don\'t have an account?")')).toBeVisible();
await expect(page.locator('a:has-text("Forgot your password?")')).toBeVisible();
// Check for Tailwind classes in form container
const formContainer = page.locator('div.max-w-md.w-full.space-y-8.bg-white.rounded-xl.shadow-lg.p-8');
await expect(formContainer).toBeVisible();
});
test('should have Tailwind CSS applied correctly', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Check for various Tailwind utility classes
const expectedClasses = [
'min-h-screen',
@ -174,21 +174,21 @@ test.describe('Login Page Debug Test', () => {
'text-sm',
'text-gray-600'
];
// Check that the page contains at least one element with Tailwind class
const bodyElement = page.locator('body');
const bodyClasses = await bodyElement.getAttribute('class');
expect(bodyClasses).toBeTruthy();
// Check for some specific Tailwind classes that should be present
expect(bodyClasses).toContain('min-h-screen');
expect(bodyClasses).toContain('bg-gray-50');
// Check that the form has appropriate Tailwind classes
const formElement = page.locator('form');
const formClasses = await formElement.getAttribute('class');
expect(formClasses).toContain('space-y-6');
// Check that inputs have Tailwind classes
const inputElement = page.locator('#email-address');
const inputClasses = await inputElement.getAttribute('class');
@ -198,33 +198,33 @@ test.describe('Login Page Debug Test', () => {
});
test('should validate form submission with empty fields', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Try submitting with empty fields
await page.click('button[type="submit"]');
// Should still be on the same page and show validation error
await expect(page).toHaveURL('http://localhost:3000/auth/login');
await expect(page).toHaveURL('http://localhost:3001/auth/login');
});
test('should have working password visibility toggle', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
const passwordInput = page.locator('#password');
const toggleButton = page.locator('button[aria-label*="toggle"]');
// Initially should be password type
await expect(passwordInput).toHaveAttribute('type', 'password');
// Click toggle button
await toggleButton.click();
// Should switch to text type
await expect(passwordInput).toHaveAttribute('type', 'text');
// Click again to toggle back
await toggleButton.click();
// Should switch back to password type
await expect(passwordInput).toHaveAttribute('type', 'password');
});

View File

@ -2,51 +2,51 @@ import { test, expect } from '@playwright/test';
test.describe('Login Page Structure and Tailwind Debug', () => {
test('should display login page with proper structure', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Verify main layout structure
await expect(page.locator('div.min-h-screen.bg-gray-50')).toBeVisible();
await expect(page.locator('div.flex.flex-col.items-center.justify-center.min-h-screen.py-12.px-4.sm:px-6.lg:px-8')).toBeVisible();
// Check header elements
await expect(page.locator('h1.text-3xl.font-bold.text-gray-900')).toBeVisible();
await expect(page.locator('p.mt-2.text-gray-600')).toContainText('Sign in to your account');
// Check form structure
await expect(page.locator('form')).toBeVisible();
await expect(page.locator('div.max-w-md.w-full.space-y-8.bg-white.rounded-xl.shadow-lg.p-8')).toBeVisible();
// Check input fields
await expect(page.locator('#email-address')).toBeVisible();
await expect(page.locator('#password')).toBeVisible();
// Check for submit button
await expect(page.locator('button[type="submit"]')).toBeVisible();
// Check links
await expect(page.locator('a[href="/auth/signup"]')).toBeVisible();
await expect(page.locator('a[href="/auth/forgot-password"]')).toBeVisible();
});
test('should have proper Tailwind CSS classes applied', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Test that the page uses Tailwind utility classes appropriately
const pageBody = await page.evaluate(() => {
const body = document.querySelector('body');
return body?.className || '';
});
// Check for essential Tailwind classes
expect(pageBody).toContain('min-h-screen');
expect(pageBody).toContain('bg-gray-50');
// Check form container classes
const formContainer = await page.evaluate(() => {
const container = document.querySelector('.max-w-md.w-full.space-y-8.bg-white.rounded-xl.shadow-lg.p-8');
return container?.className || '';
});
expect(formContainer).toContain('max-w-md');
expect(formContainer).toContain('bg-white');
expect(formContainer).toContain('rounded-xl');
@ -55,23 +55,23 @@ test.describe('Login Page Structure and Tailwind Debug', () => {
});
test('should have interactive elements working', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await page.goto('http://localhost:3001/auth/login');
// Test password visibility toggle
const passwordField = page.locator('#password');
const toggleButton = page.locator('button:has-text("Toggle password visibility")');
// Check initial state
const initialType = await passwordField.getAttribute('type');
expect(initialType).toBe('password');
// If toggle exists, test it
if (await toggleButton.isVisible()) {
await toggleButton.click();
const toggledType = await passwordField.getAttribute('type');
expect(toggledType).toBe('text');
await toggleButton.click();
const finalType = await passwordField.getAttribute('type');
expect(finalType).toBe('password');