added individual entry delete option
This commit is contained in:
151
src/App.css
151
src/App.css
@@ -642,9 +642,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.875rem;
|
||||
padding: 0.875rem 1.5rem;
|
||||
min-height: 66px;
|
||||
border-radius: 100px;
|
||||
background: #f0fdf4;
|
||||
@@ -653,11 +653,29 @@
|
||||
font-size: 1.3125rem;
|
||||
font-weight: 600;
|
||||
color: #15803d;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
opacity: 0;
|
||||
animation: save-inline-quote-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.save-inline-quote {
|
||||
font-size: 1rem;
|
||||
min-height: 52px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.save-inline-quote {
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
padding: 0 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes save-inline-quote-in {
|
||||
0% { opacity: 0; transform: scale(0.88) translateY(4px); }
|
||||
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||
@@ -1098,6 +1116,32 @@
|
||||
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
.entry-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.entry-delete-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
border-radius: 7px;
|
||||
border: 1px solid #fee2e2;
|
||||
background: #fff5f5;
|
||||
color: #ef4444;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, border-color 0.18s ease;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.entry-delete-btn:hover {
|
||||
background: #fee2e2;
|
||||
border-color: #fca5a5;
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1775,6 +1819,80 @@
|
||||
font-family: "Sniglet", system-ui;
|
||||
}
|
||||
|
||||
/* Delete confirmation modal */
|
||||
.delete-confirm-modal {
|
||||
text-align: center;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.delete-confirm-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 50%;
|
||||
background: #fee2e2;
|
||||
color: #ef4444;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.delete-confirm-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: #111827;
|
||||
font-family: "Sniglet", system-ui;
|
||||
}
|
||||
|
||||
.delete-confirm-body {
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
font-family: "Sniglet", system-ui;
|
||||
margin: 0 0 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.delete-confirm-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-confirm-cancel,
|
||||
.delete-confirm-delete {
|
||||
flex: 1;
|
||||
max-width: 10rem;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9375rem;
|
||||
font-family: "Sniglet", system-ui;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.delete-confirm-cancel {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
.delete-confirm-cancel:hover:not(:disabled) {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.delete-confirm-delete {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
.delete-confirm-delete:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.delete-confirm-cancel:disabled,
|
||||
.delete-confirm-delete:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---- Responsive: tablet+ (≥ 768px) ---- */
|
||||
@media (min-width: 768px) {
|
||||
.entry-modal-overlay {
|
||||
@@ -2449,6 +2567,33 @@
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-delete-btn {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
color: #f87171;
|
||||
}
|
||||
[data-theme="dark"] .entry-delete-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.18);
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .delete-confirm-modal {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
[data-theme="dark"] .delete-confirm-title {
|
||||
color: #e8f5e8;
|
||||
}
|
||||
[data-theme="dark"] .delete-confirm-body {
|
||||
color: #7a8a7a;
|
||||
}
|
||||
[data-theme="dark"] .delete-confirm-cancel {
|
||||
background: #252525;
|
||||
color: #b0b8b0;
|
||||
}
|
||||
[data-theme="dark"] .delete-confirm-cancel:hover:not(:disabled) {
|
||||
background: #303030;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { getUserEntries, type JournalEntry } from '../lib/api'
|
||||
import { getUserEntries, deleteEntry, type JournalEntry } from '../lib/api'
|
||||
import { decryptEntry } from '../lib/crypto'
|
||||
import { formatIST, getISTDateComponents } from '../lib/timezone'
|
||||
import BottomNav from '../components/BottomNav'
|
||||
@@ -19,6 +19,8 @@ export default function HistoryPage() {
|
||||
const [entries, setEntries] = useState<DecryptedEntry[]>([])
|
||||
const [loadingEntries, setLoadingEntries] = useState(false)
|
||||
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
||||
const [entryToDelete, setEntryToDelete] = useState<DecryptedEntry | null>(null)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
const { continueTourOnHistory } = useOnboardingTour()
|
||||
|
||||
@@ -175,6 +177,22 @@ export default function HistoryPage() {
|
||||
setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
||||
}
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!entryToDelete || !user || !userId) return
|
||||
setDeleting(true)
|
||||
try {
|
||||
const token = await user.getIdToken()
|
||||
await deleteEntry(userId, entryToDelete.id, token)
|
||||
setEntries((prev) => prev.filter((e) => e.id !== entryToDelete.id))
|
||||
if (selectedEntry?.id === entryToDelete.id) setSelectedEntry(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete entry:', error)
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
setEntryToDelete(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="history-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
@@ -267,21 +285,38 @@ export default function HistoryPage() {
|
||||
</p>
|
||||
) : (
|
||||
selectedDateEntries.map((entry) => (
|
||||
<button
|
||||
<div
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className="entry-card"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedEntry(entry)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && setSelectedEntry(entry)}
|
||||
>
|
||||
<div className="entry-header">
|
||||
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
||||
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
||||
<div className="entry-header-right">
|
||||
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="entry-delete-btn"
|
||||
title="Delete entry"
|
||||
onClick={(e) => { e.stopPropagation(); setEntryToDelete(entry) }}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" />
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="entry-title">{entry.decryptedTitle || entry.title || '[Untitled]'}</h4>
|
||||
{entry.decryptedContent && (
|
||||
<p className="entry-preview">{entry.decryptedContent}</p>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
@@ -352,6 +387,49 @@ export default function HistoryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{entryToDelete && (
|
||||
<div
|
||||
className="entry-modal-overlay"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !deleting) setEntryToDelete(null)
|
||||
}}
|
||||
>
|
||||
<div className="entry-modal delete-confirm-modal">
|
||||
<div className="delete-confirm-icon">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" />
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="delete-confirm-title">Delete entry?</h2>
|
||||
<p className="delete-confirm-body">
|
||||
"{entryToDelete.decryptedTitle || entryToDelete.title || 'Untitled'}" will be permanently deleted and cannot be recovered.
|
||||
</p>
|
||||
<div className="delete-confirm-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="delete-confirm-cancel"
|
||||
onClick={() => setEntryToDelete(null)}
|
||||
disabled={deleting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="delete-confirm-delete"
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user