Compare commits

..

10 Commits

Author SHA1 Message Date
da593e052b Update PWA version for focus improvements
- Bumped service worker cache to v8
- Updated manifest version to 1.0.2
- Forces PWA refresh to pick up keyboard focus improvements
2025-12-14 01:01:22 +00:00
37955f95ab Keep input focused and keyboard open after adding tasks
- Added useRef to manage input focus directly
- Added useEffect to auto-focus when form becomes visible
- Refocus input after successful task creation with setTimeout
- Modified handleBlur to preserve focus when keeping form open
- Provides seamless continuous task input experience on mobile
2025-12-14 00:59:48 +00:00
466bce862e Update PWA manifest and service worker to force iPhone refresh
- Updated manifest.json version to 1.0.1 and description
- Bumped service worker cache version from v6 to v7
- Forces PWA refresh on iPhone Safari to pick up latest changes
2025-12-14 00:56:29 +00:00
3043293fa7 Keep task form open after adding task for continuous input
- Modified InlineItemCreate component to maintain form visibility after successful task creation
- Added keepFormOpen state to prevent form from closing on page revalidation
- Users can now add multiple tasks without clicking 'Add task' button repeatedly
- Form only closes when explicitly cancelled or escaped
2025-12-14 00:40:59 +00:00
1838502b8b fix: prevent hydration error in NotificationPrompt
Add mounted state to prevent SSR rendering.
Component now only renders after client-side mount.
Fixes login blocking hydration error.
2025-12-07 01:59:55 +00:00
6508e27391 chore: remove NotificationDebug component
Debug complete - notifications are working (permission already granted).
Remove debug component to clean up UI.
2025-12-07 01:58:39 +00:00
15b9f196f5 chore: bump service worker cache version to v6
Force update on all PWA installations to pick up:
- NotificationPrompt component
- NotificationDebug component
- Push notification functionality
- Hydration fix
2025-12-07 01:55:28 +00:00
1c5e623c7b fix: prevent hydration error in NotificationDebug component
Add mounted state to prevent rendering during SSR.
Component now only renders after client-side mount.
2025-12-07 01:53:52 +00:00
298ebc5fb9 fix: change PushSubscription endpoint to VARCHAR for MySQL compatibility
MySQL cannot create unique constraints on TEXT fields without length.
Changed endpoint from @db.Text to @db.VarChar(512) to fix Prisma generation error.
2025-12-07 01:50:28 +00:00
374bfb3a62 .env 2025-12-07 01:48:02 +00:00
9 changed files with 78 additions and 19 deletions

18
.env Normal file
View File

@@ -0,0 +1,18 @@
DATABASE_URL="mysql://lists_app:ListsApp@2025!@192.168.4.20:3306/ultimate_lists"
# NextAuth Configuration
AUTH_URL=http://localhost:3500
NEXTAUTH_URL=http://localhost:3500
NEXTAUTH_SECRET=your-secret-key-here-please-change-in-production
AUTH_SECRET=your-secret-key-here-please-change-in-production
AUTH_TRUST_HOST=true
# Database connection check (for docker-compose health check)
DB_HOST=192.168.4.20
DB_PORT=3306
DB_USER=lists_app
DB_PASSWORD=ListsApp@2025!
VAPID_PUBLIC_KEY=BI05JNvKs4CSiG8c4q6Lt_FjMmuVy-0LWiiuvYbjnu10TpysQdVpY2LpY0gotdhlXJJLn6ub_cf4EX69gZDs83o
VAPID_PRIVATE_KEY=EUsjxWebCbrVphsDDFuoRqUE5-eX8nhpuLvYKVVeGnQ
VAPID_SUBJECT=mailto:psbailey31@gmail.com

2
.gitignore vendored
View File

@@ -23,7 +23,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
# Local env files # Local env files
.env # .env
.env*.local .env*.local
# Vercel # Vercel

View File

@@ -3,7 +3,6 @@ import './globals.css'
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration' import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
import OfflineIndicator from '@/components/OfflineIndicator' import OfflineIndicator from '@/components/OfflineIndicator'
import NotificationPrompt from '@/components/NotificationPrompt' import NotificationPrompt from '@/components/NotificationPrompt'
import NotificationDebug from '@/components/NotificationDebug'
import { ToastProvider } from '@/components/ToastContainer' import { ToastProvider } from '@/components/ToastContainer'
import ErrorBoundary from '@/components/ErrorBoundary' import ErrorBoundary from '@/components/ErrorBoundary'
@@ -78,7 +77,6 @@ export default function RootLayout({
<ServiceWorkerRegistration /> <ServiceWorkerRegistration />
<OfflineIndicator /> <OfflineIndicator />
<NotificationPrompt /> <NotificationPrompt />
<NotificationDebug />
{children} {children}
</ToastProvider> </ToastProvider>
</ErrorBoundary> </ErrorBoundary>

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useRef, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { createItem } from '@/lib/actions' import { createItem } from '@/lib/actions'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
@@ -18,6 +18,20 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [keepFormOpen, setKeepFormOpen] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
// Focus input when form becomes visible
useEffect(() => {
if ((isCreating || keepFormOpen) && inputRef.current) {
// Small delay to ensure the input is rendered
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, 50)
}
}, [isCreating, keepFormOpen])
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@@ -40,14 +54,21 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
// Clear form immediately for better UX // Clear form immediately for better UX
const itemTitle = sanitizeString(title) const itemTitle = sanitizeString(title)
setTitle('') setTitle('')
setIsCreating(false) setKeepFormOpen(true) // Mark that we want to keep the form open
// Create item and refresh // Create item and refresh
await createItem(listId, itemTitle) await createItem(listId, itemTitle)
showToast('Task added successfully', 'success') showToast('Task added successfully', 'success')
// Force immediate refresh // The revalidatePath in createItem will refresh the data
router.refresh() // but we'll keep the form open due to keepFormOpen state
// Refocus the input after a short delay to keep keyboard open
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, 100)
} catch (error) { } catch (error) {
console.error('Error creating item:', error) console.error('Error creating item:', error)
const errorMessage = error instanceof Error ? error.message : 'Failed to add task' const errorMessage = error instanceof Error ? error.message : 'Failed to add task'
@@ -63,6 +84,7 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
const handleCancel = () => { const handleCancel = () => {
setTitle('') setTitle('')
setIsCreating(false) setIsCreating(false)
setKeepFormOpen(false) // Reset the keep form open state
// Reset zoom on iOS Safari // Reset zoom on iOS Safari
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur() document.activeElement.blur()
@@ -75,12 +97,16 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
} }
} }
const handleBlur = () => { const handleBlur = (e: React.FocusEvent) => {
// Force viewport reset on iOS Safari // Only handle blur if we're not keeping the form open
window.scrollTo(0, 0) // This prevents the keyboard from closing when we want it to stay open
if (!keepFormOpen) {
// Force viewport reset on iOS Safari
window.scrollTo(0, 0)
}
} }
if (!isCreating) { if (!isCreating && !keepFormOpen) {
return ( return (
<button <button
onClick={() => setIsCreating(true)} onClick={() => setIsCreating(true)}
@@ -96,6 +122,7 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
<form onSubmit={handleSubmit} className="bg-white border-2 border-blue-400 rounded-lg p-4 sm:p-5 shadow-sm"> <form onSubmit={handleSubmit} className="bg-white border-2 border-blue-400 rounded-lg p-4 sm:p-5 shadow-sm">
<div className="mb-3 sm:mb-4"> <div className="mb-3 sm:mb-4">
<input <input
ref={inputRef}
type="text" type="text"
value={title} value={title}
onChange={(e) => { onChange={(e) => {

View File

@@ -3,9 +3,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export default function NotificationDebug() { export default function NotificationDebug() {
const [mounted, setMounted] = useState(false);
const [debug, setDebug] = useState<any>({}); const [debug, setDebug] = useState<any>({});
useEffect(() => { useEffect(() => {
setMounted(true);
const info = { const info = {
notificationSupported: 'Notification' in window, notificationSupported: 'Notification' in window,
serviceWorkerSupported: 'serviceWorker' in navigator, serviceWorkerSupported: 'serviceWorker' in navigator,
@@ -16,6 +18,10 @@ export default function NotificationDebug() {
setDebug(info); setDebug(info);
}, []); }, []);
if (!mounted) {
return null;
}
return ( return (
<div className="fixed bottom-4 left-4 bg-black text-white text-xs p-3 rounded-lg max-w-xs z-50 font-mono"> <div className="fixed bottom-4 left-4 bg-black text-white text-xs p-3 rounded-lg max-w-xs z-50 font-mono">
<div className="font-bold mb-2">Push Debug:</div> <div className="font-bold mb-2">Push Debug:</div>

View File

@@ -8,8 +8,15 @@ export default function NotificationPrompt() {
const { permission, isSubscribed, subscribe } = usePushNotifications(); const { permission, isSubscribed, subscribe } = usePushNotifications();
const [showPrompt, setShowPrompt] = useState(false); const [showPrompt, setShowPrompt] = useState(false);
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
// Check if we should show the prompt // Check if we should show the prompt
const shouldShow = const shouldShow =
'Notification' in window && 'Notification' in window &&
@@ -27,7 +34,7 @@ export default function NotificationPrompt() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [permission, isSubscribed, dismissed]); }, [mounted, permission, isSubscribed, dismissed]);
const handleEnable = async () => { const handleEnable = async () => {
const success = await subscribe(); const success = await subscribe();
@@ -42,12 +49,14 @@ export default function NotificationPrompt() {
}; };
const handleDismissForever = () => { const handleDismissForever = () => {
localStorage.setItem('notification-prompt-dismissed', 'true'); if (typeof localStorage !== 'undefined') {
localStorage.setItem('notification-prompt-dismissed', 'true');
}
setShowPrompt(false); setShowPrompt(false);
setDismissed(true); setDismissed(true);
}; };
if (!showPrompt) { if (!mounted || !showPrompt) {
return null; return null;
} }

View File

@@ -26,14 +26,14 @@ model User {
model PushSubscription { model PushSubscription {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
endpoint String @db.Text endpoint String @db.VarChar(512)
p256dh String @db.Text p256dh String @db.Text
auth String @db.Text auth String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, endpoint(255)]) @@unique([userId, endpoint])
@@index([userId]) @@index([userId])
} }

View File

@@ -1,7 +1,8 @@
{ {
"name": "Ultimate Lists", "name": "Ultimate Lists",
"short_name": "Lists", "short_name": "Lists",
"description": "The ultimate todo list app with advanced task management", "description": "The ultimate todo list app with advanced task management and continuous task input",
"version": "1.0.2",
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
"display": "standalone", "display": "standalone",

View File

@@ -1,8 +1,8 @@
// Service Worker for Ultimate Lists App // Service Worker for Ultimate Lists App
// Implements caching strategies for offline functionality // Implements caching strategies for offline functionality
const CACHE_NAME = 'ultimate-lists-v5'; const CACHE_NAME = 'ultimate-lists-v8';
const RUNTIME_CACHE = 'ultimate-lists-runtime-v5'; const RUNTIME_CACHE = 'ultimate-lists-runtime-v8';
// Static assets to cache on install // Static assets to cache on install
const STATIC_ASSETS = [ const STATIC_ASSETS = [