UI enhancement

This commit is contained in:
debu-2 2026-10-03 22:34:34 +05:30
parent 23239c5cb6
commit 53922952c6
13 changed files with 1076 additions and 260 deletions

View file

@ -1,10 +1,11 @@
import {
ChevronDown,
MoreHorizontal,
Pin,
Plus,
Trash2,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
createStickyNote,
deleteStickyNote,
@ -14,6 +15,7 @@ import bluePin from "../data/blue.png";
import greenPin from "../data/green.png";
import pinkPin from "../data/pink.png";
import yellowPin from "../data/yellow.png";
import { senderDisplayName } from "../utils/senderAvatar";
const NOTE_COLORS = ["yellow", "green", "blue", "pink"];
const PIN_IMAGES = {
@ -35,6 +37,16 @@ function formatNoteTime(value) {
}).format(date);
}
function noteMatchesContext(note, contextEmail) {
const needle = senderDisplayName(contextEmail).toLowerCase();
if (!needle) return false;
return (
note.title.toLowerCase().includes(needle)
|| note.body.toLowerCase().includes(needle)
|| note.tags.some((tag) => tag.toLowerCase().includes(needle))
);
}
function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
return (
<article className={`sticky-note sticky-note-${note.color}`}>
@ -78,11 +90,12 @@ function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
);
}
export default function StickyNotesPanel({ compact = false }) {
export default function StickyNotesPanel({ compact = false, contextEmail = null }) {
const token = sessionStorage.getItem("agmail_access_token");
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [expanded, setExpanded] = useState(false);
const [adding, setAdding] = useState(false);
const [menuOpenId, setMenuOpenId] = useState(null);
const [draft, setDraft] = useState({
@ -93,6 +106,16 @@ export default function StickyNotesPanel({ compact = false }) {
});
const [saving, setSaving] = useState(false);
const contextualNotes = useMemo(() => {
if (!contextEmail) return [];
return notes.filter((note) => noteMatchesContext(note, contextEmail));
}, [contextEmail, notes]);
const visibleNotes = useMemo(() => {
if (contextEmail && contextualNotes.length) return contextualNotes;
return notes;
}, [contextEmail, contextualNotes, notes]);
const load = useCallback(async () => {
setLoading(true);
setError("");
@ -144,6 +167,7 @@ export default function StickyNotesPanel({ compact = false }) {
color: NOTE_COLORS[(notes.length + 1) % NOTE_COLORS.length],
});
setAdding(false);
setExpanded(true);
} catch (createError) {
setError(createError.message);
} finally {
@ -177,107 +201,130 @@ export default function StickyNotesPanel({ compact = false }) {
});
}
const contextLabel = contextEmail ? senderDisplayName(contextEmail) : null;
return (
<aside className={`mailbox-notes-pane${compact ? " compact" : ""}`} aria-label="Sticky notes">
<header className="sticky-notes-header">
<div className="sticky-notes-title">
<Pin size={15} />
<h2>Sticky Notes</h2>
</div>
<div className="sticky-notes-actions">
<button
type="button"
aria-label="Add sticky note"
onClick={() => {
setDraft((current) => ({
...current,
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
}));
setAdding(true);
}}
>
<Plus size={18} />
</button>
</div>
</header>
<aside className={`mailbox-notes-pane${compact ? " compact" : ""}${expanded ? " expanded" : ""}`} aria-label="Sticky notes">
<button
type="button"
className="sticky-notes-toggle"
aria-expanded={expanded}
onClick={() => setExpanded((open) => !open)}
>
<Pin size={14} />
<span>
Notes ({notes.length})
{contextLabel && contextualNotes.length > 0 && (
<small> · {contextualNotes.length} about {contextLabel}</small>
)}
</span>
<ChevronDown size={15} className={expanded ? "open" : ""} />
</button>
<div className="sticky-notes-body">
{loading && <div className="sticky-notes-state">Loading notes…</div>}
{error && !loading && (
<div className="sticky-notes-state error" role="alert">{error}</div>
)}
{!loading && notes.map((note) => (
<NoteCard
key={note.id}
note={note}
menuOpen={menuOpenId === note.id}
onMenuToggle={(id) => setMenuOpenId((current) => (current === id ? null : id))}
onDelete={handleDelete}
/>
))}
{expanded && (
<div className="sticky-notes-body">
{contextLabel && (
<p className="sticky-notes-context">
{contextualNotes.length
? `Notes about ${contextLabel}`
: `No notes linked to ${contextLabel} yet.`}
</p>
)}
{adding ? (
<form className="sticky-note-form" onSubmit={handleCreate}>
<input
type="text"
value={draft.title}
onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))}
placeholder="Note title"
autoFocus
required
<div className="sticky-notes-toolbar">
<button
type="button"
aria-label="Add sticky note"
onClick={() => {
setDraft((current) => ({
...current,
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
tags: contextLabel && !current.tags ? contextLabel : current.tags,
}));
setAdding(true);
}}
>
<Plus size={16} />
</button>
</div>
{loading && <div className="sticky-notes-state">Loading notes…</div>}
{error && !loading && (
<div className="sticky-notes-state error" role="alert">{error}</div>
)}
{!loading && visibleNotes.map((note) => (
<NoteCard
key={note.id}
note={note}
menuOpen={menuOpenId === note.id}
onMenuToggle={(id) => setMenuOpenId((current) => (current === id ? null : id))}
onDelete={handleDelete}
/>
<textarea
value={draft.body}
onChange={(event) => setDraft((current) => ({ ...current, body: event.target.value }))}
onKeyDown={handleBodyKeyDown}
placeholder="Jot down a quick note…"
rows={3}
/>
<input
type="text"
value={draft.tags}
onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))}
placeholder="Tags (comma separated)"
/>
<div className="sticky-note-colors">
{NOTE_COLORS.map((color) => (
<button
key={color}
type="button"
className={`sticky-note-color${draft.color === color ? " active" : ""}`}
data-color={color}
aria-label={`${color} note`}
onClick={() => setDraft((current) => ({ ...current, color }))}
/>
))}
</div>
<div className="sticky-note-form-actions">
<button type="button" onClick={() => setAdding(false)} disabled={saving}>
Cancel
</button>
<button type="submit" disabled={saving || !draft.title.trim()}>
{saving ? "Saving…" : "Save note"}
</button>
</div>
</form>
) : (
<button
type="button"
className="sticky-note-add"
onClick={() => {
setDraft((current) => ({
...current,
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
}));
setAdding(true);
}}
>
<Plus size={20} />
<span>Add sticky note</span>
<small>Jot down a quick note…</small>
</button>
)}
</div>
))}
{adding ? (
<form className="sticky-note-form" onSubmit={handleCreate}>
<input
type="text"
value={draft.title}
onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))}
placeholder="Note title"
autoFocus
required
/>
<textarea
value={draft.body}
onChange={(event) => setDraft((current) => ({ ...current, body: event.target.value }))}
onKeyDown={handleBodyKeyDown}
placeholder="Jot down a quick note…"
rows={3}
/>
<input
type="text"
value={draft.tags}
onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))}
placeholder="Tags (comma separated)"
/>
<div className="sticky-note-colors">
{NOTE_COLORS.map((color) => (
<button
key={color}
type="button"
className={`sticky-note-color${draft.color === color ? " active" : ""}`}
data-color={color}
aria-label={`${color} note`}
onClick={() => setDraft((current) => ({ ...current, color }))}
/>
))}
</div>
<div className="sticky-note-form-actions">
<button type="button" onClick={() => setAdding(false)} disabled={saving}>
Cancel
</button>
<button type="submit" disabled={saving || !draft.title.trim()}>
{saving ? "Saving…" : "Save note"}
</button>
</div>
</form>
) : (
<button
type="button"
className="sticky-note-add"
onClick={() => {
setDraft((current) => ({
...current,
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
tags: contextLabel || current.tags,
}));
setAdding(true);
}}
>
<Plus size={18} />
<span>Add note</span>
</button>
)}
</div>
)}
</aside>
);
}