added individual entry delete option
This commit is contained in:
151
src/App.css
151
src/App.css
@@ -642,9 +642,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.875rem;
|
padding: 0.875rem 1.5rem;
|
||||||
min-height: 66px;
|
min-height: 66px;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
background: #f0fdf4;
|
background: #f0fdf4;
|
||||||
@@ -653,11 +653,29 @@
|
|||||||
font-size: 1.3125rem;
|
font-size: 1.3125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #15803d;
|
color: #15803d;
|
||||||
white-space: nowrap;
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: save-inline-quote-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
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 {
|
@keyframes save-inline-quote-in {
|
||||||
0% { opacity: 0; transform: scale(0.88) translateY(4px); }
|
0% { opacity: 0; transform: scale(0.88) translateY(4px); }
|
||||||
100% { opacity: 1; transform: scale(1) translateY(0); }
|
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
@@ -1098,6 +1116,32 @@
|
|||||||
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.09);
|
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 {
|
.entry-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1775,6 +1819,80 @@
|
|||||||
font-family: "Sniglet", system-ui;
|
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) ---- */
|
/* ---- Responsive: tablet+ (≥ 768px) ---- */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.entry-modal-overlay {
|
.entry-modal-overlay {
|
||||||
@@ -2449,6 +2567,33 @@
|
|||||||
color: #f87171;
|
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 {
|
[data-theme="dark"] .entry-modal-overlay {
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
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 { decryptEntry } from '../lib/crypto'
|
||||||
import { formatIST, getISTDateComponents } from '../lib/timezone'
|
import { formatIST, getISTDateComponents } from '../lib/timezone'
|
||||||
import BottomNav from '../components/BottomNav'
|
import BottomNav from '../components/BottomNav'
|
||||||
@@ -19,6 +19,8 @@ export default function HistoryPage() {
|
|||||||
const [entries, setEntries] = useState<DecryptedEntry[]>([])
|
const [entries, setEntries] = useState<DecryptedEntry[]>([])
|
||||||
const [loadingEntries, setLoadingEntries] = useState(false)
|
const [loadingEntries, setLoadingEntries] = useState(false)
|
||||||
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
||||||
|
const [entryToDelete, setEntryToDelete] = useState<DecryptedEntry | null>(null)
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
|
||||||
const { continueTourOnHistory } = useOnboardingTour()
|
const { continueTourOnHistory } = useOnboardingTour()
|
||||||
|
|
||||||
@@ -175,6 +177,22 @@ export default function HistoryPage() {
|
|||||||
setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="history-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div className="history-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
@@ -267,21 +285,38 @@ export default function HistoryPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
selectedDateEntries.map((entry) => (
|
selectedDateEntries.map((entry) => (
|
||||||
<button
|
<div
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
type="button"
|
|
||||||
className="entry-card"
|
className="entry-card"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => setSelectedEntry(entry)}
|
onClick={() => setSelectedEntry(entry)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && setSelectedEntry(entry)}
|
||||||
>
|
>
|
||||||
<div className="entry-header">
|
<div className="entry-header">
|
||||||
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
<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>
|
</div>
|
||||||
<h4 className="entry-title">{entry.decryptedTitle || entry.title || '[Untitled]'}</h4>
|
<h4 className="entry-title">{entry.decryptedTitle || entry.title || '[Untitled]'}</h4>
|
||||||
{entry.decryptedContent && (
|
{entry.decryptedContent && (
|
||||||
<p className="entry-preview">{entry.decryptedContent}</p>
|
<p className="entry-preview">{entry.decryptedContent}</p>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -352,6 +387,49 @@ export default function HistoryPage() {
|
|||||||
</div>
|
</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 />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user