Compare commits
8 Commits
298ebc5fb9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| da593e052b | |||
| 37955f95ab | |||
| 466bce862e | |||
| 3043293fa7 | |||
| 1838502b8b | |||
| 6508e27391 | |||
| 15b9f196f5 | |||
| 1c5e623c7b |
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user