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) - Set cookie with security flags (httpOnly, secure, sameSite)
- Use jose library instead of jsonwebtoken to avoid CommonJS issues with Edge runtime - Use jose library instead of jsonwebtoken to avoid CommonJS issues with Edge runtime
</action> </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> <done>User can successfully sign up and receive a valid session token in HTTP-only cookie</done>
</task> </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 - Use jose library instead of jsonwebtoken to avoid CommonJS issues with Edge runtime
- Prevent timing attacks by comparing passwords in constant time - Prevent timing attacks by comparing passwords in constant time
</action> </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> <done>User can successfully authenticate and receive a valid session token in HTTP-only cookie</done>
</task> </task>
@ -164,7 +164,7 @@ We need to implement the full authentication logic and ensure proper session man
- Return success message to the client - Return success message to the client
- Ensure cookie flags match those used during login (httpOnly, secure, sameSite) - Ensure cookie flags match those used during login (httpOnly, secure, sameSite)
</action> </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> <done>User can successfully log out and session cookie is cleared</done>
</task> </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) - Send email using configured email service (details in infrastructure)
- Not reveal if email exists in system to prevent enumeration - Not reveal if email exists in system to prevent enumeration
</action> </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> <done>User can request password reset and receive confirmation without revealing account existence</done>
</task> </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 - Return appropriate error responses for invalid or expired tokens
- Use bcryptjs for password hashing - Use bcryptjs for password hashing
</action> </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> <done>User can complete password reset with valid token and receive confirmation</done>
</task> </task>
@ -148,7 +148,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Clear token from database after verification - Clear token from database after verification
- Handle invalid/missing/expired tokens gracefully - Handle invalid/missing/expired tokens gracefully
</action> </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> <done>User can verify email address with valid token and receive confirmation</done>
</task> </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 - Be styled consistently with the rest of the application
- Have a link back to login page - Have a link back to login page
</action> </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> <done>Forgot password page is accessible, visually consistent, and functional</done>
</task> </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 - Be styled consistently with the rest of the application
- Have a link back to login page - Have a link back to login page
</action> </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> <done>Password reset page is accessible, visually consistent, and functional</done>
</task> </task>
@ -213,7 +213,7 @@ Building upon the foundation established in Plan 1, this plan implements the rem
- Provide clear feedback on verification status - Provide clear feedback on verification status
- Be styled consistently with the rest of the application - Be styled consistently with the rest of the application
</action> </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> <done>Email verification page is accessible, visually consistent, and functional</done>
</task> </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) - Send email using configured email service (details in infrastructure)
- Return clear success/failure messages - Return clear success/failure messages
</action> </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> <done>User can request verification email to be resent and receive confirmation</done>
</task> </task>

View File

@ -85,7 +85,7 @@ Output: Bidirectional synchronization logic, sync API endpoint, and calendar set
- Handles errors gracefully with appropriate messaging - Handles errors gracefully with appropriate messaging
Follow the API endpoint pattern established in Phase 2 and authentication flow from Phase 1.</action> 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> <done>Endpoint successfully triggers sync process and returns status JSON</done>
</task> </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 - Handle and log token exchange errors appropriately
- Store the tokens in the CalendarConnection database record - Store the tokens in the CalendarConnection database record
</action> </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> <done>Google OAuth callback endpoint exchanges real tokens and stores them in database</done>
</task> </task>
@ -102,7 +102,7 @@ Functional OAuth endpoints that exchange authorization codes for real tokens and
- Handle and log token exchange errors appropriately - Handle and log token exchange errors appropriately
- Store the tokens in the CalendarConnection database record - Store the tokens in the CalendarConnection database record
</action> </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> <done>Apple OAuth callback endpoint exchanges real tokens and stores them in database</done>
</task> </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 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ 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"] CMD ["npm", "run", "start"]

View File

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

View File

@ -39,7 +39,7 @@ This will create all necessary tables:
npm run dev npm run dev
``` ```
Visit: **http://localhost:3000** Visit: **http://localhost:3001**
--- ---
@ -47,14 +47,14 @@ Visit: **http://localhost:3000**
### Sign Up (Email/Password) ### 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) 2. Enter email and password (min 8 characters)
3. Click "Sign up" 3. Click "Sign up"
4. You'll be automatically signed in 4. You'll be automatically signed in
### Sign In (Email/Password) ### Sign In (Email/Password)
1. Go to http://localhost:3000/auth/login 1. Go to http://localhost:3001/auth/login
2. Enter your credentials 2. Enter your credentials
3. Click "Sign in" 3. Click "Sign in"
@ -82,7 +82,7 @@ To enable "Sign in with Google":
4. Click **Create Credentials** → **OAuth client ID** 4. Click **Create Credentials** → **OAuth client ID**
5. Choose **Web application** 5. Choose **Web application**
6. Add these redirect URIs: 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/auth/callback/google` (via Reverse Proxy)
- `https://todo.martin-bierschenk.de/api/calendar/google/oauth` (via Reverse Proxy) - `https://todo.martin-bierschenk.de/api/calendar/google/oauth` (via Reverse Proxy)
7. Copy your **Client ID** and **Client Secret** 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 ## Assumptions
1. Application runs on localhost:3000 (development server) 1. Application runs on localhost:3001 (development server)
2. All components are properly rendered 2. All components are properly rendered
3. Network connectivity allows for API calls 3. Network connectivity allows for API calls
4. Required environment variables are configured 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 ## Implementation Notes
The test plan assumes: 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 - All components are properly implemented and accessible
- Environment variables are configured appropriately - Environment variables are configured appropriately
- Network connectivity supports API interactions - Network connectivity supports API interactions

View File

@ -1,9 +1,9 @@
// Debug script to inspect login page structure and Tailwind CSS // 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 ==='); 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 // 1. Check if page loads properly
console.log('1. Checking page load...'); console.log('1. Checking page load...');
@ -23,7 +23,7 @@ console.log('\n3. Checking login elements:');
const h1Elements = document.querySelectorAll('h1'); const h1Elements = document.querySelectorAll('h1');
console.log('H1 elements count:', h1Elements.length); console.log('H1 elements count:', h1Elements.length);
h1Elements.forEach((el, i) => { h1Elements.forEach((el, i) => {
console.log(` H1 ${i+1}: "${el.textContent}"`); console.log(` H1 ${i + 1}: "${el.textContent}"`);
}); });
// 4. Check for form elements // 4. Check for form elements
@ -59,7 +59,7 @@ const inputs = document.querySelectorAll('input');
console.log('Inputs found:', inputs.length); console.log('Inputs found:', inputs.length);
inputs.forEach((input, i) => { inputs.forEach((input, i) => {
if (input.type === 'email' || input.type === 'password') { 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}`); console.log(` Classes: ${input.className}`);
} }
}); });
@ -67,7 +67,7 @@ inputs.forEach((input, i) => {
const buttons = document.querySelectorAll('button'); const buttons = document.querySelectorAll('button');
console.log('Buttons found:', buttons.length); console.log('Buttons found:', buttons.length);
buttons.forEach((btn, i) => { 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 // 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. * This script is meant to be run in the browser console to debug signup issues.
* *
* To use: * To use:
* 1. Navigate to http://localhost:3000/auth/signup * 1. Navigate to http://localhost:3001/auth/signup
* 2. Open browser developer tools (F12) * 2. Open browser developer tools (F12)
* 3. Go to Console tab * 3. Go to Console tab
* 4. Paste this entire script and run it * 4. Paste this entire script and run it
@ -30,7 +30,7 @@ if (!signupForm) {
}); });
} else { } else {
console.log('✅ Form found:', signupForm); console.log('✅ Form found:', signupForm);
// Check form attributes // Check form attributes
console.log('Form attributes:', { console.log('Form attributes:', {
action: signupForm.action, action: signupForm.action,
@ -38,28 +38,28 @@ if (!signupForm) {
id: signupForm.id, id: signupForm.id,
className: signupForm.className className: signupForm.className
}); });
// Add temporary event listener to see if submit is being called // 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('📋 Form submit intercepted');
console.log('Submit event:', e); console.log('Submit event:', e);
console.log('Form data will be sent to:', this.action || 'default'); console.log('Form data will be sent to:', this.action || 'default');
}, { once: true }); }, { once: true });
// Check input fields // Check input fields
const emailInput = document.querySelector('input[name="email"]'); const emailInput = document.querySelector('input[name="email"]');
const passwordInput = document.querySelector('input[name="password"]'); const passwordInput = document.querySelector('input[name="password"]');
const submitButton = document.querySelector('button[type="submit"]'); const submitButton = document.querySelector('button[type="submit"]');
console.log('Input fields:'); console.log('Input fields:');
if (emailInput) console.log('✅ Email input:', emailInput); if (emailInput) console.log('✅ Email input:', emailInput);
if (passwordInput) console.log('✅ Password input:', passwordInput); if (passwordInput) console.log('✅ Password input:', passwordInput);
if (submitButton) console.log('✅ Submit button:', submitButton); if (submitButton) console.log('✅ Submit button:', submitButton);
// Monitor network requests // Monitor network requests
console.log('🔍 Monitoring network requests...'); console.log('🔍 Monitoring network requests...');
const originalFetch = window.fetch; const originalFetch = window.fetch;
window.fetch = function(...args) { window.fetch = function (...args) {
console.log('📡 Fetch called with:', args); console.log('📡 Fetch called with:', args);
const [input, init] = args; const [input, init] = args;
console.log(' URL:', input); console.log(' URL:', input);
@ -69,9 +69,9 @@ if (!signupForm) {
} }
return originalFetch.apply(this, args); return originalFetch.apply(this, args);
}; };
// Monitor for errors // Monitor for errors
window.addEventListener('error', function(e) { window.addEventListener('error', function (e) {
console.error('🚨 JS Error caught:', e.error); console.error('🚨 JS Error caught:', e.error);
console.error('Error location:', e.filename, e.lineno, e.colno); 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 # Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 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 # For Next.js, run in production mode
CMD ["npm", "run", "start"] CMD ["npm", "run", "start"]

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "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

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

View File

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

View File

@ -39,6 +39,7 @@ import {
Palette, Palette,
Sparkles, Sparkles,
Info, Info,
Trash2,
} from "lucide-react"; } from "lucide-react";
// Types // 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 // Main Component
export default function WeeklyView() { export default function WeeklyView() {
const { data: session } = useSession(); const { data: session } = useSession();
@ -558,6 +582,7 @@ export default function WeeklyView() {
yearFontSize?: string; yearFontSize?: string;
yearFontWeight?: string; yearFontWeight?: string;
yearColor?: string; yearColor?: string;
quoteSourceUrls?: string[];
}>({ }>({
name: session?.user?.name || "", name: session?.user?.name || "",
email: session?.user?.email || "", email: session?.user?.email || "",
@ -609,6 +634,7 @@ export default function WeeklyView() {
weekendColorSun: "#dc2626", weekendColorSun: "#dc2626",
pastDayColor: "#a6a6a7", pastDayColor: "#a6a6a7",
quoteSourceUrl: "https://recite.vercel.app/api/random", quoteSourceUrl: "https://recite.vercel.app/api/random",
quoteSourceUrls: ["https://recite.vercel.app/api/random"],
}); });
const [motivationalQuote, setMotivationalQuote] = useState(""); const [motivationalQuote, setMotivationalQuote] = useState("");
const [showSummary, setShowSummary] = useState(false); const [showSummary, setShowSummary] = useState(false);
@ -945,31 +971,42 @@ export default function WeeklyView() {
const fetchMotivationalQuote = useCallback(async () => { const fetchMotivationalQuote = useCallback(async () => {
if (profile.goalFallbackType !== "quote") return; 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) const urls = profile.quoteSourceUrls && profile.quoteSourceUrls.length > 0
let quoteText = ""; ? profile.quoteSourceUrls
if (Array.isArray(data) && data.length > 0) { : [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"];
const item = data[0];
quoteText = item.quote || item.text || item.content || (typeof item === 'string' ? item : ""); // Strategy: try sources until one works
if (item.author) quoteText += ` - ${item.author}`; for (const url of urls) {
} else if (data && typeof data === 'object') { try {
quoteText = data.quote || data.text || data.content || ""; const res = await fetch(url);
if (data.author) quoteText += ` - ${data.author}`; if (!res.ok) continue;
} else if (typeof data === 'string') { const data = await res.json();
quoteText = data;
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 // Fetch tasks on mount
useEffect(() => { useEffect(() => {
@ -1457,15 +1494,17 @@ export default function WeeklyView() {
const dateStr = formatDateToISO(date); // Use local date formatting const dateStr = formatDateToISO(date); // Use local date formatting
return tasks return tasks
.filter((task) => { .filter((task) => {
if (!task.scheduledDate) return false; if (!task.completed && task.scheduledDate) {
// Exclude sub-tasks from top-level list (they render inside their parent) // Exclude sub-tasks from top-level list (they render inside their parent)
if (task.parentTaskId) return false; if (task.parentTaskId) return false;
// Use string comparison to avoid timezone shifts // Use string comparison to avoid timezone shifts
const taskDateStr = const taskDateStr =
typeof task.scheduledDate === "string" typeof task.scheduledDate === "string"
? task.scheduledDate.substring(0, 10) ? task.scheduledDate.substring(0, 10)
: formatDateToISO(new Date(task.scheduledDate)); : formatDateToISO(new Date(task.scheduledDate));
return taskDateStr === dateStr; return taskDateStr === dateStr;
}
return false;
}) })
.sort((a, b) => { .sort((a, b) => {
// Sort by time if available // Sort by time if available
@ -1798,6 +1837,9 @@ export default function WeeklyView() {
const taskDuration = task.duration || 15; const taskDuration = task.duration || 15;
const taskEnd = taskStart + taskDuration; 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 // Overlap condition: task starts before slot ends AND task ends after slot starts
return taskStart < slotEnd && taskEnd > slotStart; return taskStart < slotEnd && taskEnd > slotStart;
}); });
@ -3270,27 +3312,39 @@ export default function WeeklyView() {
</div> </div>
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */} {/* 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 */} {/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2"> <div className="whitespace-nowrap flex items-center gap-2">
{(isLoading || isSyncing || syncStatus === "syncing") && ( {(isLoading || isSyncing || syncStatus === "syncing") ? (
<div className="weekly-spinner" title="Syncing..."></div> <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={{ <span style={{
fontFamily: profile.cwFontFamily || "Inter", fontFamily: profile.cwFontFamily || "Inter",
fontSize: profile.cwFontSize || "1.125rem", fontSize: profile.cwFontSize || "1.125rem",
fontWeight: Number(profile.cwFontWeight || "700"), 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")} KW {getWeekNumber(currentWeekStart).toString().padStart(2, "0")}
</span> </span>
<span className="text-gray-400">|</span> <span className="text-gray-400">|</span>
<span style={{ <span
fontFamily: profile.yearFontFamily || "Inter", style={{
fontSize: profile.yearFontSize || "1.125rem", fontFamily: profile.yearFontFamily || "Inter",
fontWeight: Number(profile.yearFontWeight || "700"), fontSize: profile.yearFontSize || "1.125rem",
color: profile.yearColor || "#333333", fontWeight: Number(profile.yearFontWeight || "700"),
}}> color: adjustColorForDarkMode(profile.yearColor || "#333333", darkMode),
filter: "brightness(var(--weekly-header-brightness, 1))"
}}
>
{currentWeekStart.getFullYear()} {currentWeekStart.getFullYear()}
</span> </span>
</div> </div>
@ -3330,6 +3384,8 @@ export default function WeeklyView() {
: undefined, : undefined,
fontSize: profile.goalFontSize || undefined, fontSize: profile.goalFontSize || undefined,
fontWeight: profile.goalFontWeight || undefined, fontWeight: profile.goalFontWeight || undefined,
color: adjustColorForDarkMode((profile.goalFallbackType === "quote" ? profile.taskColor : undefined) || "#333333", darkMode),
filter: "brightness(var(--weekly-goal-brightness, 1))"
}} }}
> >
{showNextTask {showNextTask
@ -5640,7 +5696,15 @@ function TaskItem({
</svg> </svg>
</span> </span>
)} )}
<span className="truncate">{task.title}</span> <span
style={{
whiteSpace: "normal",
wordBreak: "break-word",
overflow: "visible"
}}
>
{task.title}
</span>
</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"> <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; yearFontSize?: string;
yearFontWeight?: string; yearFontWeight?: string;
yearColor?: string; yearColor?: string;
quoteSourceUrls: string[];
}) => void; }) => void;
quoteSourceUrls?: string[];
goal: string; goal: string;
setGoal: (goal: string) => void; setGoal: (goal: string) => void;
saveGoal: (goal: string) => void; saveGoal: (goal: string) => void;
@ -6288,6 +6354,7 @@ function SettingsSidebar({
yearFontSize?: string; yearFontSize?: string;
yearFontWeight?: string; yearFontWeight?: string;
yearColor?: string; yearColor?: string;
quoteSourceUrls?: string[];
}>({ }>({
name: "", name: "",
email: "", email: "",
@ -8995,7 +9062,7 @@ function SettingsSidebar({
flex: 1, flex: 1,
padding: "10px", padding: "10px",
borderRadius: "6px", borderRadius: "6px",
border: "none", border: profile.goalScope === "week" ? "2px solid var(--weekly-teal)" : "none",
background: background:
profile.goalScope === "week" profile.goalScope === "week"
? "var(--weekly-settings-toggle-active-bg)" ? "var(--weekly-settings-toggle-active-bg)"
@ -9004,7 +9071,8 @@ function SettingsSidebar({
profile.goalScope === "week" profile.goalScope === "week"
? "var(--weekly-settings-toggle-active-text)" ? "var(--weekly-settings-toggle-active-text)"
: "var(--weekly-settings-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", cursor: "pointer",
transition: "all 0.2s", transition: "all 0.2s",
}} }}
@ -9020,7 +9088,7 @@ function SettingsSidebar({
flex: 1, flex: 1,
padding: "10px", padding: "10px",
borderRadius: "6px", borderRadius: "6px",
border: "none", border: profile.goalScope === "day" ? "2px solid var(--weekly-teal)" : "none",
background: background:
profile.goalScope === "day" profile.goalScope === "day"
? "var(--weekly-settings-toggle-active-bg)" ? "var(--weekly-settings-toggle-active-bg)"
@ -9029,7 +9097,8 @@ function SettingsSidebar({
profile.goalScope === "day" profile.goalScope === "day"
? "var(--weekly-settings-toggle-active-text)" ? "var(--weekly-settings-toggle-active-text)"
: "var(--weekly-settings-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", cursor: "pointer",
transition: "all 0.2s", transition: "all 0.2s",
}} }}
@ -9104,28 +9173,73 @@ function SettingsSidebar({
color: "var(--weekly-settings-label)", color: "var(--weekly-settings-label)",
}} }}
> >
API-Datenquelle (URL) API-Datenquellen (URLs)
</label> </label>
<input <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
type="text" {(profile.quoteSourceUrls || [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"]).map((url: string, idx: number) => (
value={profile.quoteSourceUrl || ""} <div key={idx} style={{ display: "flex", gap: "8px" }}>
onChange={(e) => <input
setProfile((p) => ({ type="text"
...p, value={url}
quoteSourceUrl: e.target.value, onChange={(e) => {
})) const newUrls = [...(profile.quoteSourceUrls || [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"])];
} newUrls[idx] = e.target.value;
className="weekly-input" setProfile((p) => ({ ...p, quoteSourceUrls: newUrls }));
placeholder="https://recite.vercel.app/api/random" }}
style={{ className="weekly-input"
width: "100%", placeholder="https://..."
padding: "10px", style={{
fontSize: "0.95rem", flex: 1,
borderRadius: "6px", padding: "10px",
border: "1px solid var(--weekly-border)", fontSize: "0.95rem",
background: "var(--weekly-bg)", 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" }}> <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":"..."}'}). URL that returns a JSON list or object of quotes (e.g. {'[{"quote":"...","author":"..."}]'} or {'{"quote":"..."}'}).
</p> </p>

View File

@ -4,18 +4,18 @@
* *
* Usage: * Usage:
* 1. Start the development server: npm run dev * 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: * 3. Fill in the form with:
* - Email: mail@martin-bierschenk.de * - Email: mail@martin-bierschenk.de
* - Password: My-Weekly-ToDo-List * - Password: My-Weekly-ToDo-List
* 4. Or use the following curl command: * 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" \ * -H "Content-Type: application/json" \
* -d '{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}' * -d '{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}'
* *
* For login after signup: * 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" \ * -H "Content-Type: application/json" \
* -d '{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}' * -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('Password: My-Weekly-ToDo-List');
console.log(''); console.log('');
console.log('Method 1: Using Browser Interface'); 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('2. Fill in the form with the credentials above');
console.log('3. Submit the form'); console.log('3. Submit the form');
console.log(''); console.log('');
console.log('Method 2: Using curl commands (in terminal)'); console.log('Method 2: Using curl commands (in terminal)');
console.log('First, create the account:'); 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(' -H "Content-Type: application/json" \\');
console.log(' -d \'{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}\''); console.log(' -d \'{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}\'');
console.log(''); console.log('');
console.log('Then, authenticate with the account:'); 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(' -H "Content-Type: application/json" \\');
console.log(' -d \'{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}\''); console.log(' -d \'{"email":"mail@martin-bierschenk.de","password":"My-Weekly-ToDo-List"}\'');
console.log(''); console.log('');

View File

@ -5,9 +5,9 @@ const fetch = require('node-fetch');
async function testSignupApi() { async function testSignupApi() {
console.log('Testing signup API endpoint...'); console.log('Testing signup API endpoint...');
try { try {
const response = await fetch('http://localhost:3000/api/auth/signup', { const response = await fetch('http://localhost:3001/api/auth/signup', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -17,13 +17,13 @@ async function testSignupApi() {
password: 'testpassword' password: 'testpassword'
}) })
}); });
console.log('Status:', response.status); console.log('Status:', response.status);
console.log('Headers:', response.headers.raw()); console.log('Headers:', response.headers.raw());
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType); console.log('Content-Type:', contentType);
if (contentType && contentType.includes('application/json')) { if (contentType && contentType.includes('application/json')) {
const data = await response.json(); const data = await response.json();
console.log('JSON Response:', data); console.log('JSON Response:', data);
@ -32,7 +32,7 @@ async function testSignupApi() {
console.log('Non-JSON Response (likely HTML):'); console.log('Non-JSON Response (likely HTML):');
console.log(text.substring(0, 500) + '...'); console.log(text.substring(0, 500) + '...');
} }
} catch (error) { } catch (error) {
console.error('Error testing API:', 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 authentication flow against your running application
test.describe('Authentication Flow Tests', () => { test.describe('Authentication Flow Tests', () => {
test('should access login page', async ({ page }) => { 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 // Check that we're on the login page
await expect(page).toHaveTitle(/Login/); await expect(page).toHaveTitle(/Login/);
// Check for login form elements // Check for login form elements
await expect(page.getByText('Login')).toBeVisible(); await expect(page.getByText('Login')).toBeVisible();
await expect(page.getByPlaceholder('Email')).toBeVisible(); await expect(page.getByPlaceholder('Email')).toBeVisible();
@ -16,11 +16,11 @@ test.describe('Authentication Flow Tests', () => {
}); });
test('should access signup page', async ({ page }) => { 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 // Check that we're on the signup page
await expect(page).toHaveTitle(/Sign Up/); await expect(page).toHaveTitle(/Sign Up/);
// Check for signup form elements // Check for signup form elements
await expect(page.getByText('Sign Up')).toBeVisible(); await expect(page.getByText('Sign Up')).toBeVisible();
await expect(page.getByPlaceholder('Email')).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 to verify CSS styling is working in your application
test.describe('CSS Debugging Tests', () => { test.describe('CSS Debugging Tests', () => {
test('should load with proper CSS styling', async ({ page }) => { 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 // Verify main page elements have proper styling
const mainHeader = page.getByText('My Weekly To Do List'); const mainHeader = page.getByText('My Weekly To Do List');
await expect(mainHeader).toBeVisible(); await expect(mainHeader).toBeVisible();
// Check for CSS classes that indicate styling is applied // Check for CSS classes that indicate styling is applied
const taskItem = page.locator('.task-item'); const taskItem = page.locator('.task-item');
await expect(taskItem).toBeVisible(); await expect(taskItem).toBeVisible();
// Verify basic CSS properties are applied // Verify basic CSS properties are applied
const taskItemStyles = await taskItem.evaluate(element => { const taskItemStyles = await taskItem.evaluate(element => {
const computedStyle = window.getComputedStyle(element); const computedStyle = window.getComputedStyle(element);
@ -22,18 +22,18 @@ test.describe('CSS Debugging Tests', () => {
boxShadow: computedStyle.boxShadow boxShadow: computedStyle.boxShadow
}; };
}); });
console.log('Task item CSS properties:', taskItemStyles); console.log('Task item CSS properties:', taskItemStyles);
expect(taskItemStyles.backgroundColor).toBeTruthy(); expect(taskItemStyles.backgroundColor).toBeTruthy();
}); });
test('should have proper auth form styling', async ({ page }) => { 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 // Check that auth form has proper styling
const authForm = page.locator('.auth-form'); const authForm = page.locator('.auth-form');
await expect(authForm).toBeVisible(); await expect(authForm).toBeVisible();
// Verify CSS properties on auth form // Verify CSS properties on auth form
const formStyles = await authForm.evaluate(element => { const formStyles = await authForm.evaluate(element => {
const computedStyle = window.getComputedStyle(element); const computedStyle = window.getComputedStyle(element);
@ -43,7 +43,7 @@ test.describe('CSS Debugging Tests', () => {
boxShadow: computedStyle.boxShadow boxShadow: computedStyle.boxShadow
}; };
}); });
console.log('Auth form CSS properties:', formStyles); console.log('Auth form CSS properties:', formStyles);
expect(formStyles.backgroundColor).toBeTruthy(); 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 // Simple test to verify the app is running and accessible
test('My Weekly To Do List should be accessible', async ({ page }) => { test('My Weekly To Do List should be accessible', async ({ page }) => {
// Navigate to the running application // Navigate to the running application
await page.goto('http://localhost:3000'); await page.goto('http://localhost:3001');
// Check that the main elements are present // Check that the main elements are present
await expect(page).toHaveTitle(/My Weekly To Do List/); await expect(page).toHaveTitle(/My Weekly To Do List/);
// Check for main UI elements // Check for main UI elements
await expect(page.getByText('My Weekly To Do List')).toBeVisible(); await expect(page.getByText('My Weekly To Do List')).toBeVisible();
await expect(page.getByText('Weekly View')).toBeVisible(); await expect(page.getByText('Weekly View')).toBeVisible();
}); });
test('Should be able to access tasks page', async ({ page }) => { 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 // Try to navigate to tasks page
const tasksLink = page.getByText('Tasks'); const tasksLink = page.getByText('Tasks');
await expect(tasksLink).toBeVisible(); await expect(tasksLink).toBeVisible();

View File

@ -2,50 +2,50 @@ import { test, expect } from '@playwright/test';
test.describe('Login Page Core Functionality', () => { test.describe('Login Page Core Functionality', () => {
test('should load the login page with core elements', async ({ page }) => { 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 // Basic page verification
await expect(page).toHaveURL(/login/); await expect(page).toHaveURL(/login/);
await expect(page).toHaveTitle(/Weekly To Do List/); await expect(page).toHaveTitle(/Weekly To Do List/);
// Check that the page has essential structure // Check that the page has essential structure
const h1Element = page.locator('h1'); const h1Element = page.locator('h1');
await expect(h1Element).toBeVisible(); await expect(h1Element).toBeVisible();
// Check for a login-related paragraph // Check for a login-related paragraph
const loginParagraph = page.getByText(/Sign in to your account|Welcome back!/); const loginParagraph = page.getByText(/Sign in to your account|Welcome back!/);
await expect(loginParagraph).toBeVisible(); await expect(loginParagraph).toBeVisible();
// Check for form elements // Check for form elements
const emailInput = page.locator('input[name="email"]'); const emailInput = page.locator('input[name="email"]');
const passwordInput = page.locator('input[name="password"]'); const passwordInput = page.locator('input[name="password"]');
const submitButton = page.locator('button[type="submit"]'); const submitButton = page.locator('button[type="submit"]');
await expect(emailInput).toBeVisible(); await expect(emailInput).toBeVisible();
await expect(passwordInput).toBeVisible(); await expect(passwordInput).toBeVisible();
await expect(submitButton).toBeVisible(); await expect(submitButton).toBeVisible();
// Check for links // Check for links
const signupLink = page.getByRole('link', { name: /Don't have an account|Create account/ }); const signupLink = page.getByRole('link', { name: /Don't have an account|Create account/ });
const forgotPasswordLink = page.getByRole('link', { name: /Forgot your password/ }); const forgotPasswordLink = page.getByRole('link', { name: /Forgot your password/ });
await expect(signupLink).toBeVisible(); await expect(signupLink).toBeVisible();
await expect(forgotPasswordLink).toBeVisible(); await expect(forgotPasswordLink).toBeVisible();
console.log('✅ Login page loaded successfully with core elements'); console.log('✅ Login page loaded successfully with core elements');
}); });
test('should have basic CSS structure', async ({ page }) => { 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 // Check that the page has basic styling structure
const body = page.locator('body'); const body = page.locator('body');
await expect(body).toBeVisible(); await expect(body).toBeVisible();
// Check for main wrapper div that should have Tailwind classes // Check for main wrapper div that should have Tailwind classes
const mainWrapper = page.locator('[class*="min-h-screen"]'); const mainWrapper = page.locator('[class*="min-h-screen"]');
await expect(mainWrapper).toBeVisible(); await expect(mainWrapper).toBeVisible();
console.log('✅ Basic CSS structure verified'); 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.describe('Login Page Debug - Simplified', () => {
test('should load login page successfully', async ({ page }) => { 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 // Basic page load verification
await expect(page).toHaveURL(/login/); await expect(page).toHaveURL(/login/);
await expect(page).toHaveTitle(/Weekly To Do List/); await expect(page).toHaveTitle(/Weekly To Do List/);
// Check for key elements that should be present // Check for key elements that should be present
await expect(page.locator('h1')).toContainText('Weekly To Do List'); await expect(page.locator('h1')).toContainText('Weekly To Do List');
await expect(page.locator('p')).toContainText('Sign in to your account'); await expect(page.locator('p')).toContainText('Sign in to your account');
// Check for form elements // Check for form elements
await expect(page.locator('input[name="email"]')).toBeVisible(); await expect(page.locator('input[name="email"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible(); await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toBeVisible(); await expect(page.locator('button[type="submit"]')).toBeVisible();
// Check for navigation links // Check for navigation links
await expect(page.getByRole('link', { name: 'Don\'t have an account?' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Don\'t have an account?' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Forgot your password?' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Forgot your password?' })).toBeVisible();
}); });
test('should have Tailwind CSS classes in structure', async ({ page }) => { 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 // Check main container classes
const container = page.locator('div.min-h-screen.bg-gray-50'); const container = page.locator('div.min-h-screen.bg-gray-50');
await expect(container).toBeVisible(); await expect(container).toBeVisible();
// Check the form container // Check the form container
const formContainer = page.locator('div.bg-white.rounded-xl.shadow-lg.p-8'); const formContainer = page.locator('div.bg-white.rounded-xl.shadow-lg.p-8');
await expect(formContainer).toBeVisible(); await expect(formContainer).toBeVisible();
// Check for basic Tailwind classes that should be present // Check for basic Tailwind classes that should be present
const bodyClass = await page.evaluate(() => document.body.className); const bodyClass = await page.evaluate(() => document.body.className);
expect(bodyClass).toContain('min-h-screen'); expect(bodyClass).toContain('min-h-screen');
@ -40,19 +40,19 @@ test.describe('Login Page Debug - Simplified', () => {
}); });
test('should have functional form elements', async ({ page }) => { 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 // Test that the form exists
const form = page.locator('form'); const form = page.locator('form');
await expect(form).toBeVisible(); await expect(form).toBeVisible();
// Test that we can interact with inputs // Test that we can interact with inputs
const emailInput = page.locator('input[name="email"]'); const emailInput = page.locator('input[name="email"]');
const passwordInput = page.locator('input[name="password"]'); const passwordInput = page.locator('input[name="password"]');
await expect(emailInput).toBeVisible(); await expect(emailInput).toBeVisible();
await expect(passwordInput).toBeVisible(); await expect(passwordInput).toBeVisible();
// Test that submit button exists // Test that submit button exists
const submitButton = page.locator('button[type="submit"]'); const submitButton = page.locator('button[type="submit"]');
await expect(submitButton).toBeVisible(); await expect(submitButton).toBeVisible();

View File

@ -2,50 +2,50 @@ import { test, expect } from '@playwright/test';
test.describe('Login Page Debug Test', () => { test.describe('Login Page Debug Test', () => {
test('should load the login page correctly', async ({ page }) => { 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 // Check page title and heading
await expect(page).toHaveTitle(/Weekly To Do List/); await expect(page).toHaveTitle(/Weekly To Do List/);
await expect(page.locator('h1')).toContainText('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(); await expect(page.locator('p:has-text("Sign in to your account")')).toBeVisible();
// Check for Tailwind CSS classes in main container // Check for Tailwind CSS classes in main container
const mainContainer = page.locator('div.min-h-screen.bg-gray-50'); const mainContainer = page.locator('div.min-h-screen.bg-gray-50');
await expect(mainContainer).toBeVisible(); await expect(mainContainer).toBeVisible();
// Check for form elements // Check for form elements
const form = page.locator('form'); const form = page.locator('form');
await expect(form).toBeVisible(); await expect(form).toBeVisible();
// Check for email input field // Check for email input field
const emailInput = page.locator('#email-address'); const emailInput = page.locator('#email-address');
await expect(emailInput).toBeVisible(); await expect(emailInput).toBeVisible();
await expect(emailInput).toHaveAttribute('type', 'email'); await expect(emailInput).toHaveAttribute('type', 'email');
await expect(emailInput).toHaveAttribute('placeholder', 'Email address'); await expect(emailInput).toHaveAttribute('placeholder', 'Email address');
// Check for password input field // Check for password input field
const passwordInput = page.locator('#password'); const passwordInput = page.locator('#password');
await expect(passwordInput).toBeVisible(); await expect(passwordInput).toBeVisible();
await expect(passwordInput).toHaveAttribute('type', 'password'); await expect(passwordInput).toHaveAttribute('type', 'password');
await expect(passwordInput).toHaveAttribute('placeholder', 'Password'); await expect(passwordInput).toHaveAttribute('placeholder', 'Password');
// Check for submit button // Check for submit button
const submitButton = page.locator('button[type="submit"]'); const submitButton = page.locator('button[type="submit"]');
await expect(submitButton).toBeVisible(); await expect(submitButton).toBeVisible();
await expect(submitButton).toContainText('Sign in'); await expect(submitButton).toContainText('Sign in');
// Check for links // Check for links
await expect(page.locator('a:has-text("Don\'t have an account?")')).toBeVisible(); await expect(page.locator('a:has-text("Don\'t have an account?")')).toBeVisible();
await expect(page.locator('a:has-text("Forgot your password?")')).toBeVisible(); await expect(page.locator('a:has-text("Forgot your password?")')).toBeVisible();
// Check for Tailwind classes in form container // 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'); 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(); await expect(formContainer).toBeVisible();
}); });
test('should have Tailwind CSS applied correctly', async ({ page }) => { 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 // Check for various Tailwind utility classes
const expectedClasses = [ const expectedClasses = [
'min-h-screen', 'min-h-screen',
@ -174,21 +174,21 @@ test.describe('Login Page Debug Test', () => {
'text-sm', 'text-sm',
'text-gray-600' 'text-gray-600'
]; ];
// Check that the page contains at least one element with Tailwind class // Check that the page contains at least one element with Tailwind class
const bodyElement = page.locator('body'); const bodyElement = page.locator('body');
const bodyClasses = await bodyElement.getAttribute('class'); const bodyClasses = await bodyElement.getAttribute('class');
expect(bodyClasses).toBeTruthy(); expect(bodyClasses).toBeTruthy();
// Check for some specific Tailwind classes that should be present // Check for some specific Tailwind classes that should be present
expect(bodyClasses).toContain('min-h-screen'); expect(bodyClasses).toContain('min-h-screen');
expect(bodyClasses).toContain('bg-gray-50'); expect(bodyClasses).toContain('bg-gray-50');
// Check that the form has appropriate Tailwind classes // Check that the form has appropriate Tailwind classes
const formElement = page.locator('form'); const formElement = page.locator('form');
const formClasses = await formElement.getAttribute('class'); const formClasses = await formElement.getAttribute('class');
expect(formClasses).toContain('space-y-6'); expect(formClasses).toContain('space-y-6');
// Check that inputs have Tailwind classes // Check that inputs have Tailwind classes
const inputElement = page.locator('#email-address'); const inputElement = page.locator('#email-address');
const inputClasses = await inputElement.getAttribute('class'); 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 }) => { 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 // Try submitting with empty fields
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
// Should still be on the same page and show validation error // 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 }) => { 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 passwordInput = page.locator('#password');
const toggleButton = page.locator('button[aria-label*="toggle"]'); const toggleButton = page.locator('button[aria-label*="toggle"]');
// Initially should be password type // Initially should be password type
await expect(passwordInput).toHaveAttribute('type', 'password'); await expect(passwordInput).toHaveAttribute('type', 'password');
// Click toggle button // Click toggle button
await toggleButton.click(); await toggleButton.click();
// Should switch to text type // Should switch to text type
await expect(passwordInput).toHaveAttribute('type', 'text'); await expect(passwordInput).toHaveAttribute('type', 'text');
// Click again to toggle back // Click again to toggle back
await toggleButton.click(); await toggleButton.click();
// Should switch back to password type // Should switch back to password type
await expect(passwordInput).toHaveAttribute('type', 'password'); 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.describe('Login Page Structure and Tailwind Debug', () => {
test('should display login page with proper structure', async ({ page }) => { 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 // Verify main layout structure
await expect(page.locator('div.min-h-screen.bg-gray-50')).toBeVisible(); 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(); 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 // Check header elements
await expect(page.locator('h1.text-3xl.font-bold.text-gray-900')).toBeVisible(); 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'); await expect(page.locator('p.mt-2.text-gray-600')).toContainText('Sign in to your account');
// Check form structure // Check form structure
await expect(page.locator('form')).toBeVisible(); 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(); 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 // Check input fields
await expect(page.locator('#email-address')).toBeVisible(); await expect(page.locator('#email-address')).toBeVisible();
await expect(page.locator('#password')).toBeVisible(); await expect(page.locator('#password')).toBeVisible();
// Check for submit button // Check for submit button
await expect(page.locator('button[type="submit"]')).toBeVisible(); await expect(page.locator('button[type="submit"]')).toBeVisible();
// Check links // Check links
await expect(page.locator('a[href="/auth/signup"]')).toBeVisible(); await expect(page.locator('a[href="/auth/signup"]')).toBeVisible();
await expect(page.locator('a[href="/auth/forgot-password"]')).toBeVisible(); await expect(page.locator('a[href="/auth/forgot-password"]')).toBeVisible();
}); });
test('should have proper Tailwind CSS classes applied', async ({ page }) => { 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 // Test that the page uses Tailwind utility classes appropriately
const pageBody = await page.evaluate(() => { const pageBody = await page.evaluate(() => {
const body = document.querySelector('body'); const body = document.querySelector('body');
return body?.className || ''; return body?.className || '';
}); });
// Check for essential Tailwind classes // Check for essential Tailwind classes
expect(pageBody).toContain('min-h-screen'); expect(pageBody).toContain('min-h-screen');
expect(pageBody).toContain('bg-gray-50'); expect(pageBody).toContain('bg-gray-50');
// Check form container classes // Check form container classes
const formContainer = await page.evaluate(() => { 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'); const container = document.querySelector('.max-w-md.w-full.space-y-8.bg-white.rounded-xl.shadow-lg.p-8');
return container?.className || ''; return container?.className || '';
}); });
expect(formContainer).toContain('max-w-md'); expect(formContainer).toContain('max-w-md');
expect(formContainer).toContain('bg-white'); expect(formContainer).toContain('bg-white');
expect(formContainer).toContain('rounded-xl'); 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 }) => { 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 // Test password visibility toggle
const passwordField = page.locator('#password'); const passwordField = page.locator('#password');
const toggleButton = page.locator('button:has-text("Toggle password visibility")'); const toggleButton = page.locator('button:has-text("Toggle password visibility")');
// Check initial state // Check initial state
const initialType = await passwordField.getAttribute('type'); const initialType = await passwordField.getAttribute('type');
expect(initialType).toBe('password'); expect(initialType).toBe('password');
// If toggle exists, test it // If toggle exists, test it
if (await toggleButton.isVisible()) { if (await toggleButton.isVisible()) {
await toggleButton.click(); await toggleButton.click();
const toggledType = await passwordField.getAttribute('type'); const toggledType = await passwordField.getAttribute('type');
expect(toggledType).toBe('text'); expect(toggledType).toBe('text');
await toggleButton.click(); await toggleButton.click();
const finalType = await passwordField.getAttribute('type'); const finalType = await passwordField.getAttribute('type');
expect(finalType).toBe('password'); expect(finalType).toBe('password');