edit entry option added
This commit is contained in:
160
src/App.css
160
src/App.css
@@ -1141,6 +1141,26 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-edit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.625rem;
|
||||||
|
height: 1.625rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #3b82f6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.18s ease, border-color 0.18s ease;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.entry-edit-btn:hover {
|
||||||
|
background: #dbeafe;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
.entry-delete-btn {
|
.entry-delete-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1922,6 +1942,115 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edit entry modal */
|
||||||
|
.entry-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-modal-edit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #3b82f6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.entry-modal-edit:hover {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-modal {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-title-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border: 1.5px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #111827;
|
||||||
|
background: #f9fafb;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.edit-entry-title-input:focus {
|
||||||
|
border-color: #6ee7b7;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-content-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border: 1.5px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-family: "Sniglet", system-ui;
|
||||||
|
color: #374151;
|
||||||
|
background: #f9fafb;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.edit-entry-content-input:focus {
|
||||||
|
border-color: #6ee7b7;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.edit-entry-content-input:disabled,
|
||||||
|
.edit-entry-title-input:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-save {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 10rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
background: #10b981;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.edit-entry-save:hover:not(:disabled) {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
.edit-entry-save: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 {
|
||||||
@@ -2698,6 +2827,25 @@
|
|||||||
color: #f87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .entry-edit-btn {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
border-color: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .entry-edit-btn:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.18);
|
||||||
|
border-color: rgba(59, 130, 246, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .entry-modal-edit {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .entry-modal-edit:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .entry-delete-btn {
|
[data-theme="dark"] .entry-delete-btn {
|
||||||
background: rgba(239, 68, 68, 0.08);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
border-color: rgba(239, 68, 68, 0.2);
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
@@ -2708,6 +2856,18 @@
|
|||||||
border-color: rgba(239, 68, 68, 0.35);
|
border-color: rgba(239, 68, 68, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .edit-entry-title-input,
|
||||||
|
[data-theme="dark"] .edit-entry-content-input {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #2d2d2d;
|
||||||
|
color: #e8f5e8;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .edit-entry-title-input:focus,
|
||||||
|
[data-theme="dark"] .edit-entry-content-input:focus {
|
||||||
|
border-color: #4ade80;
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .delete-confirm-modal {
|
[data-theme="dark"] .delete-confirm-modal {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { getUserEntries, deleteEntry, type JournalEntry } from '../lib/api'
|
import { getUserEntries, deleteEntry, updateEntry, type JournalEntry } from '../lib/api'
|
||||||
import { decryptEntry } from '../lib/crypto'
|
import { decryptEntry, encryptEntry } 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'
|
||||||
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
||||||
@@ -22,6 +22,10 @@ export default function HistoryPage() {
|
|||||||
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
const [selectedEntry, setSelectedEntry] = useState<DecryptedEntry | null>(null)
|
||||||
const [entryToDelete, setEntryToDelete] = useState<DecryptedEntry | null>(null)
|
const [entryToDelete, setEntryToDelete] = useState<DecryptedEntry | null>(null)
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const [entryToEdit, setEntryToEdit] = useState<DecryptedEntry | null>(null)
|
||||||
|
const [editTitle, setEditTitle] = useState('')
|
||||||
|
const [editContent, setEditContent] = useState('')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const { continueTourOnHistory } = useOnboardingTour()
|
const { continueTourOnHistory } = useOnboardingTour()
|
||||||
|
|
||||||
@@ -178,6 +182,58 @@ export default function HistoryPage() {
|
|||||||
setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
setSelectedDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEntryFromToday = (createdAt: string): boolean => {
|
||||||
|
const nowIST = new Date(new Date().getTime() + 5.5 * 60 * 60 * 1000)
|
||||||
|
const components = getISTDateComponents(createdAt)
|
||||||
|
return (
|
||||||
|
components.year === nowIST.getUTCFullYear() &&
|
||||||
|
components.month === nowIST.getUTCMonth() &&
|
||||||
|
components.date === nowIST.getUTCDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditModal = (entry: DecryptedEntry) => {
|
||||||
|
setEntryToEdit(entry)
|
||||||
|
setEditTitle(entry.decryptedTitle || '')
|
||||||
|
setEditContent(entry.decryptedContent || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditSave = async () => {
|
||||||
|
if (!entryToEdit || !user || !userId || !secretKey) return
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const token = await user.getIdToken()
|
||||||
|
const combined = `${editTitle.trim()}\n\n${editContent.trim()}`
|
||||||
|
const { ciphertext, nonce } = await encryptEntry(combined, secretKey)
|
||||||
|
|
||||||
|
await updateEntry(userId, entryToEdit.id, {
|
||||||
|
title: undefined,
|
||||||
|
content: undefined,
|
||||||
|
encryption: {
|
||||||
|
encrypted: true,
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
algorithm: 'XSalsa20-Poly1305',
|
||||||
|
},
|
||||||
|
}, token)
|
||||||
|
|
||||||
|
const updatedEntry: DecryptedEntry = {
|
||||||
|
...entryToEdit,
|
||||||
|
encryption: { encrypted: true, ciphertext, nonce, algorithm: 'XSalsa20-Poly1305' },
|
||||||
|
decryptedTitle: editTitle.trim(),
|
||||||
|
decryptedContent: editContent.trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntries((prev) => prev.map((e) => e.id === entryToEdit.id ? updatedEntry : e))
|
||||||
|
if (selectedEntry?.id === entryToEdit.id) setSelectedEntry(updatedEntry)
|
||||||
|
setEntryToEdit(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update entry:', error)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!entryToDelete || !user || !userId) return
|
if (!entryToDelete || !user || !userId) return
|
||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
@@ -291,6 +347,19 @@ export default function HistoryPage() {
|
|||||||
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
<span className="entry-date">{formatDate(entry.createdAt)}</span>
|
||||||
<div className="entry-header-right">
|
<div className="entry-header-right">
|
||||||
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
<span className="entry-time">{formatTime(entry.createdAt)}</span>
|
||||||
|
{isEntryFromToday(entry.createdAt) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="entry-edit-btn"
|
||||||
|
title="Edit entry"
|
||||||
|
onClick={(e) => { e.stopPropagation(); openEditModal(entry) }}
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="entry-delete-btn"
|
className="entry-delete-btn"
|
||||||
@@ -332,17 +401,32 @@ export default function HistoryPage() {
|
|||||||
<span className="entry-modal-date">{formatDate(selectedEntry.createdAt)}</span>
|
<span className="entry-modal-date">{formatDate(selectedEntry.createdAt)}</span>
|
||||||
<span className="entry-modal-time">{formatTime(selectedEntry.createdAt)}</span>
|
<span className="entry-modal-time">{formatTime(selectedEntry.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="entry-modal-actions">
|
||||||
type="button"
|
{isEntryFromToday(selectedEntry.createdAt) && (
|
||||||
className="entry-modal-close"
|
<button
|
||||||
onClick={() => setSelectedEntry(null)}
|
type="button"
|
||||||
title="Close"
|
className="entry-modal-edit"
|
||||||
>
|
onClick={() => { setSelectedEntry(null); openEditModal(selectedEntry) }}
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
title="Edit entry"
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
>
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
</svg>
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
</button>
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="entry-modal-close"
|
||||||
|
onClick={() => setSelectedEntry(null)}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="entry-modal-title">
|
<h2 className="entry-modal-title">
|
||||||
@@ -381,6 +465,71 @@ export default function HistoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Edit Entry Modal */}
|
||||||
|
{entryToEdit && (
|
||||||
|
<div
|
||||||
|
className="entry-modal-overlay"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && !saving) setEntryToEdit(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="entry-modal edit-entry-modal">
|
||||||
|
<div className="entry-modal-header">
|
||||||
|
<span className="entry-modal-date">Edit Entry</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="entry-modal-close"
|
||||||
|
onClick={() => setEntryToEdit(null)}
|
||||||
|
disabled={saving}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="edit-entry-fields">
|
||||||
|
<input
|
||||||
|
className="edit-entry-title-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Title"
|
||||||
|
value={editTitle}
|
||||||
|
onChange={(e) => setEditTitle(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="edit-entry-content-input"
|
||||||
|
placeholder="What are you grateful for today?"
|
||||||
|
value={editContent}
|
||||||
|
onChange={(e) => setEditContent(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
rows={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="edit-entry-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="delete-confirm-cancel"
|
||||||
|
onClick={() => setEntryToEdit(null)}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="edit-entry-save"
|
||||||
|
onClick={handleEditSave}
|
||||||
|
disabled={saving || (!editTitle.trim() && !editContent.trim())}
|
||||||
|
>
|
||||||
|
{saving ? 'Saving…' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{entryToDelete && (
|
{entryToDelete && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user