Files
parths-life-manager/src/components/modules/NotesView.tsx
T

354 lines
14 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import {
BookOpen,
Search,
Plus,
Trash2,
Tag,
Pin,
Smile,
X,
Sparkles,
Heart
} from 'lucide-react';
import { Note } from '@/lib/types';
import { getFormattedDate } from '@/lib/initialData';
import { playNotificationChime } from '@/lib/audio';
interface NotesViewProps {
notes: Note[];
onAddNote: (note: Omit<Note, 'id' | 'updatedAt'>) => void;
onUpdateNote: (id: string, updates: Partial<Note>) => void;
onDeleteNote: (id: string) => void;
showAddModalDefault?: boolean;
}
export const NotesView: React.FC<NotesViewProps> = ({
notes,
onAddNote,
onUpdateNote,
onDeleteNote,
showAddModalDefault = false,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [isModalOpen, setIsModalOpen] = useState(showAddModalDefault);
// Form state
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [tagsInput, setTagsInput] = useState('');
const [category, setCategory] = useState('Tech');
const [mood, setMood] = useState<1 | 2 | 3 | 4 | 5>(4);
const [pinned, setPinned] = useState(false);
const moodEmojis: Record<number, { emoji: string; label: string }> = {
1: { emoji: '😫', label: 'Stressed' },
2: { emoji: '😴', label: 'Tired' },
3: { emoji: '😐', label: 'Neutral' },
4: { emoji: '😊', label: 'Energized' },
5: { emoji: '🚀', label: 'Unstoppable' },
};
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
const tags = tagsInput
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
onAddNote({
title: title.trim(),
content: content.trim(),
tags,
category: category.trim() || 'General',
mood,
pinned,
});
setTitle('');
setContent('');
setTagsInput('');
setIsModalOpen(false);
playNotificationChime('click');
};
const categories = ['all', ...Array.from(new Set(notes.map((n) => n.category)))];
const filteredNotes = notes.filter((n) => {
const matchesSearch =
n.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.tags.some((t) => t.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCat = selectedCategory === 'all' || n.category === selectedCategory;
return matchesSearch && matchesCat;
});
// Sort pinned first
const sortedNotes = [...filteredNotes].sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
return (
<div className="space-y-6">
{/* Top Bar: Search, Category Filter, New Note Button */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
{/* Search */}
<div className="relative w-full sm:w-72">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Search notes or tags..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 transition-colors"
/>
</div>
{/* Category Pills */}
<div className="flex items-center gap-2 overflow-x-auto w-full sm:w-auto pb-1 sm:pb-0">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-1.5 rounded-xl text-xs font-semibold capitalize transition-all shrink-0 ${
selectedCategory === cat
? 'bg-purple-500/20 text-purple-300 border border-purple-500/40'
: 'bg-slate-900 text-slate-400 border border-slate-800 hover:text-slate-200'
}`}
>
{cat}
</button>
))}
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-bold shadow-lg shadow-purple-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer shrink-0 ml-auto sm:ml-2"
>
<Plus className="w-4 h-4" />
<span>New Note</span>
</button>
</div>
</div>
{/* Notes Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sortedNotes.map((note) => (
<div
key={note.id}
className={`glass-card p-6 flex flex-col justify-between group transition-all relative ${
note.pinned ? 'border-purple-500/40 shadow-purple-500/10' : ''
}`}
>
<div>
{/* Header: Category, Mood, Pin, Delete */}
<div className="flex items-start justify-between gap-2 mb-3">
<span className="text-[11px] font-bold uppercase tracking-wider px-2.5 py-0.5 rounded-md bg-purple-500/10 text-purple-300 border border-purple-500/30">
{note.category}
</span>
<div className="flex items-center gap-2">
{note.mood && (
<span
className="text-sm bg-slate-900 px-2 py-0.5 rounded-md border border-slate-800"
title={`Mood: ${moodEmojis[note.mood]?.label}`}
>
{moodEmojis[note.mood]?.emoji}
</span>
)}
<button
onClick={() => onUpdateNote(note.id, { pinned: !note.pinned })}
className={`p-1 rounded transition-colors ${
note.pinned ? 'text-purple-400' : 'text-slate-500 hover:text-slate-300'
}`}
title={note.pinned ? "Unpin note" : "Pin note"}
>
<Pin className="w-3.5 h-3.5" />
</button>
<button
onClick={() => onDeleteNote(note.id)}
className="p-1 text-slate-500 hover:text-red-400 rounded transition-colors"
title="Delete note"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Note Title */}
<h4 className="text-base font-bold text-slate-100 mb-2">{note.title}</h4>
{/* Note Content */}
<div className="text-xs text-slate-300 leading-relaxed font-mono whitespace-pre-wrap bg-slate-950/40 p-3 rounded-xl border border-slate-900 max-h-48 overflow-y-auto">
{note.content}
</div>
</div>
{/* Footer: Tags & Date */}
<div className="mt-4 pt-3 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<div className="flex flex-wrap items-center gap-1.5">
{note.tags.map((t) => (
<span key={t} className="inline-flex items-center gap-1 text-[10px] text-slate-400 bg-slate-900 px-1.5 py-0.5 rounded border border-slate-800">
<Tag className="w-2.5 h-2.5 text-purple-400" />
{t}
</span>
))}
</div>
<span>{note.updatedAt}</span>
</div>
</div>
))}
{sortedNotes.length === 0 && (
<div className="col-span-full py-16 text-center text-slate-500 space-y-2">
<BookOpen className="w-8 h-8 mx-auto text-slate-600" />
<p className="text-sm">No notes found matching your criteria.</p>
<button
onClick={() => setIsModalOpen(true)}
className="text-purple-400 hover:text-purple-300 text-xs font-semibold"
>
Write your first thought
</button>
</div>
)}
</div>
{/* Create Note Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-200">
<div className="relative w-full max-w-lg rounded-2xl bg-[#0e1424] border border-purple-500/30 p-6 shadow-2xl space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-slate-800">
<h3 className="text-base font-bold text-white flex items-center gap-2">
<BookOpen className="w-4 h-4 text-purple-400" />
Capture Note & Reflection
</h3>
<button
onClick={() => setIsModalOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Title *
</label>
<input
type="text"
required
placeholder="e.g. Architectural Principles, Today's Reflection"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Content (Markdown Supported) *
</label>
<textarea
rows={5}
required
placeholder="Write your thoughts, code snippets, learnings, or daily reflection..."
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:border-purple-500 font-mono resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Category
</label>
<input
type="text"
placeholder="Tech, Mindset, Life"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Tags (comma separated)
</label>
<input
type="text"
placeholder="React, Vision, 2026"
value={tagsInput}
onChange={(e) => setTagsInput(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-purple-500"
/>
</div>
</div>
{/* Mood Selector */}
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Daily Mood Pulse
</label>
<div className="grid grid-cols-5 gap-2">
{([1, 2, 3, 4, 5] as const).map((m) => (
<button
key={m}
type="button"
onClick={() => setMood(m)}
className={`p-2 rounded-xl border flex flex-col items-center gap-1 transition-all ${
mood === m
? 'bg-purple-500/20 border-purple-500 shadow-md shadow-purple-500/20'
: 'bg-slate-900 border-slate-800 hover:border-slate-700'
}`}
>
<span className="text-xl">{moodEmojis[m].emoji}</span>
<span className="text-[10px] text-slate-400 font-medium">{moodEmojis[m].label}</span>
</button>
))}
</div>
</div>
<div className="flex items-center gap-2 pt-1">
<input
type="checkbox"
id="pinnedCheck"
checked={pinned}
onChange={(e) => setPinned(e.target.checked)}
className="rounded border-slate-700 text-purple-600 focus:ring-purple-500"
/>
<label htmlFor="pinnedCheck" className="text-xs text-slate-300 cursor-pointer">
Pin to top of list
</label>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2 rounded-xl bg-gradient-to-r from-purple-500 to-indigo-600 text-white text-xs font-bold shadow-md shadow-purple-500/20 hover:scale-105 active:scale-95 transition-all cursor-pointer"
>
Save Note
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};