Compare commits
10 Commits
e095568fbf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| da593e052b | |||
| 37955f95ab | |||
| 466bce862e | |||
| 3043293fa7 | |||
| 1838502b8b | |||
| 6508e27391 | |||
| 15b9f196f5 | |||
| 1c5e623c7b | |||
| 298ebc5fb9 | |||
| 374bfb3a62 |
18
.env
Normal file
18
.env
Normal 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
2
.gitignore
vendored
@@ -23,7 +23,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
# .env
|
||||
.env*.local
|
||||
|
||||
# Vercel
|
||||
|
||||
@@ -3,7 +3,6 @@ import './globals.css'
|
||||
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
||||
import OfflineIndicator from '@/components/OfflineIndicator'
|
||||
import NotificationPrompt from '@/components/NotificationPrompt'
|
||||
import NotificationDebug from '@/components/NotificationDebug'
|
||||
import { ToastProvider } from '@/components/ToastContainer'
|
||||
import ErrorBoundary from '@/components/ErrorBoundary'
|
||||
|
||||
@@ -78,7 +77,6 @@ export default function RootLayout({
|
||||
<ServiceWorkerRegistration />
|
||||
<OfflineIndicator />
|
||||
<NotificationPrompt />
|
||||
<NotificationDebug />
|
||||
{children}
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { createItem } from '@/lib/actions'
|
||||
import { Plus } from 'lucide-react'
|
||||
@@ -18,6 +18,20 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
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) => {
|
||||
e.preventDefault()
|
||||
@@ -40,14 +54,21 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
|
||||
// Clear form immediately for better UX
|
||||
const itemTitle = sanitizeString(title)
|
||||
setTitle('')
|
||||
setIsCreating(false)
|
||||
setKeepFormOpen(true) // Mark that we want to keep the form open
|
||||
|
||||
// Create item and refresh
|
||||
await createItem(listId, itemTitle)
|
||||
showToast('Task added successfully', 'success')
|
||||
|
||||
// Force immediate refresh
|
||||
router.refresh()
|
||||
// The revalidatePath in createItem will refresh the data
|
||||
// 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) {
|
||||
console.error('Error creating item:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to add task'
|
||||
@@ -63,6 +84,7 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
|
||||
const handleCancel = () => {
|
||||
setTitle('')
|
||||
setIsCreating(false)
|
||||
setKeepFormOpen(false) // Reset the keep form open state
|
||||
// Reset zoom on iOS Safari
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
@@ -75,12 +97,16 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// Force viewport reset on iOS Safari
|
||||
window.scrollTo(0, 0)
|
||||
const handleBlur = (e: React.FocusEvent) => {
|
||||
// Only handle blur if we're not keeping the form open
|
||||
// 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 (
|
||||
<button
|
||||
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">
|
||||
<div className="mb-3 sm:mb-4">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function NotificationDebug() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [debug, setDebug] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const info = {
|
||||
notificationSupported: 'Notification' in window,
|
||||
serviceWorkerSupported: 'serviceWorker' in navigator,
|
||||
@@ -16,6 +18,10 @@ export default function NotificationDebug() {
|
||||
setDebug(info);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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="font-bold mb-2">Push Debug:</div>
|
||||
|
||||
@@ -8,8 +8,15 @@ export default function NotificationPrompt() {
|
||||
const { permission, isSubscribed, subscribe } = usePushNotifications();
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
// Check if we should show the prompt
|
||||
const shouldShow =
|
||||
'Notification' in window &&
|
||||
@@ -27,7 +34,7 @@ export default function NotificationPrompt() {
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [permission, isSubscribed, dismissed]);
|
||||
}, [mounted, permission, isSubscribed, dismissed]);
|
||||
|
||||
const handleEnable = async () => {
|
||||
const success = await subscribe();
|
||||
@@ -42,12 +49,14 @@ export default function NotificationPrompt() {
|
||||
};
|
||||
|
||||
const handleDismissForever = () => {
|
||||
localStorage.setItem('notification-prompt-dismissed', 'true');
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('notification-prompt-dismissed', 'true');
|
||||
}
|
||||
setShowPrompt(false);
|
||||
setDismissed(true);
|
||||
};
|
||||
|
||||
if (!showPrompt) {
|
||||
if (!mounted || !showPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,14 @@ model User {
|
||||
model PushSubscription {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
endpoint String @db.Text
|
||||
endpoint String @db.VarChar(512)
|
||||
p256dh String @db.Text
|
||||
auth String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, endpoint(255)])
|
||||
@@unique([userId, endpoint])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "Ultimate 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": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Service Worker for Ultimate Lists App
|
||||
// Implements caching strategies for offline functionality
|
||||
|
||||
const CACHE_NAME = 'ultimate-lists-v5';
|
||||
const RUNTIME_CACHE = 'ultimate-lists-runtime-v5';
|
||||
const CACHE_NAME = 'ultimate-lists-v8';
|
||||
const RUNTIME_CACHE = 'ultimate-lists-runtime-v8';
|
||||
|
||||
// Static assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
|
||||
Reference in New Issue
Block a user