added 1RM and macro calculators to fitness dashboard.
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user