Implement Phase 1: Daily Helper Foundation

Complete implementation of Phase 1 enhancements including:

Backend:
- UserProfile model for storing user preferences (timezone, trading style, risk tolerance)
- DailyRoutine model for scheduling routines (morning, active_trading, evening)
- RoutineExecution model for tracking routine execution history
- Notification model for managing all types of notifications
- DailyChecklist model for daily task tracking with completion percentage
- HabitTracker model for tracking habits and streaks

Services:
- RoutineService: Handles routine execution with task registry pattern
- RoutineScheduler: Async scheduler for automated routine execution
- NotificationService: Comprehensive notification creation and delivery system
- Support for price alerts, news, routines, reminders, and performance notifications

API Endpoints (daily_helper router):
- User profile: CRUD operations, get/update preferences
- Daily routines: Create, list, execute, track history
- Notifications: CRUD, mark read, batch operations
- Daily checklists: CRUD, item management, completion tracking
- Habits: Create, track, log completions, manage streaks
- Dashboard: Summary endpoint for daily helper overview

Frontend Components:
- UserProfileSetup: Complete user profile configuration with preferences
- NotificationCenter: Bell icon with dropdown, notification management
- HabitTracker: Habit creation, streak tracking, gamification with fire emojis
- DailyChecklistPanel: Checklist management with completion percentage

Schemas:
- Full Pydantic schemas for request/response validation
- Type-safe API contracts

Features:
- Timezone support for international users
- Trading style and risk tolerance preferences
- Automated routine execution with task registry
- Real-time notifications with priority levels
- Habit streaks with motivational badges
- Daily checklist with persistent state
- Completion percentage tracking
- Notes and metadata support

All components are production-ready with error handling and user feedback.
This commit is contained in:
Claude
2025-11-15 23:09:10 +00:00
parent 31ece179d5
commit 7dd2166bf4
10 changed files with 2766 additions and 2 deletions
@@ -0,0 +1,364 @@
import React, { useState, useEffect } from 'react';
import { CheckCircle2, Circle, Plus, X } from 'lucide-react';
interface ChecklistItem {
id: string;
title: string;
completed: boolean;
completed_at?: string;
}
interface DailyChecklist {
id: number;
checklist_date: string;
checklist_type: string;
items: ChecklistItem[];
completion_percentage: number;
notes?: string;
created_at: string;
updated_at: string;
}
interface DailyChecklistPanelProps {
checklistType?: 'morning' | 'active_trading' | 'evening' | 'all';
}
const DailyChecklistPanel: React.FC<DailyChecklistPanelProps> = ({
checklistType = 'morning',
}) => {
const [checklist, setChecklist] = useState<DailyChecklist | null>(null);
const [loading, setLoading] = useState(false);
const [newItemTitle, setNewItemTitle] = useState('');
const [showAddForm, setShowAddForm] = useState(false);
const [notes, setNotes] = useState('');
useEffect(() => {
loadChecklist();
// Refresh every minute
const interval = setInterval(loadChecklist, 60000);
return () => clearInterval(interval);
}, [checklistType]);
const loadChecklist = async () => {
try {
setLoading(true);
const response = await fetch('/api/daily-helper/checklists/today');
if (response.ok) {
const data: DailyChecklist | null = await response.json();
if (data) {
setChecklist(data);
setNotes(data.notes || '');
} else {
// Create default checklist
await createDefaultChecklist();
}
}
} catch (err) {
console.error('Failed to load checklist:', err);
} finally {
setLoading(false);
}
};
const createDefaultChecklist = async () => {
const defaultItems = {
morning: [
{ id: '1', title: 'Check Economic Calendar', completed: false },
{ id: '2', title: 'Scan Market News', completed: false },
{ id: '3', title: 'Analyze Market Sentiment', completed: false },
{ id: '4', title: 'Identify Key Levels', completed: false },
{ id: '5', title: 'Create Trading Plan', completed: false },
],
active_trading: [
{ id: '1', title: 'Monitor Price Action', completed: false },
{ id: '2', title: 'Execute Per Plan', completed: false },
{ id: '3', title: 'Manage Open Positions', completed: false },
{ id: '4', title: 'Track Breaking News', completed: false },
{ id: '5', title: 'Log Trades', completed: false },
],
evening: [
{ id: '1', title: 'Review All Trades', completed: false },
{ id: '2', title: 'Complete Trading Journal', completed: false },
{ id: '3', title: 'Analyze Performance', completed: false },
{ id: '4', title: 'Update Key Levels', completed: false },
{ id: '5', title: 'Plan for Tomorrow', completed: false },
],
};
const type = (checklistType === 'all' ? 'morning' : checklistType) as keyof typeof defaultItems;
try {
const response = await fetch('/api/daily-helper/checklists', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
checklist_type: type,
items: defaultItems[type],
notes: '',
}),
});
if (response.ok) {
const data = await response.json();
setChecklist(data);
}
} catch (err) {
console.error('Failed to create default checklist:', err);
}
};
const handleToggleItem = async (itemId: string, completed: boolean) => {
if (!checklist) return;
try {
const response = await fetch(
`/api/daily-helper/checklists/${checklist.id}/items/${itemId}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !completed }),
}
);
if (response.ok) {
const updated = await response.json();
setChecklist(updated);
}
} catch (err) {
console.error('Failed to update checklist item:', err);
}
};
const handleAddItem = async (e: React.FormEvent) => {
e.preventDefault();
if (!checklist || !newItemTitle.trim()) return;
const newItem: ChecklistItem = {
id: Date.now().toString(),
title: newItemTitle,
completed: false,
};
const updatedItems = [...checklist.items, newItem];
try {
const response = await fetch(`/api/daily-helper/checklists/${checklist.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: updatedItems,
}),
});
if (response.ok) {
const updated = await response.json();
setChecklist(updated);
setNewItemTitle('');
setShowAddForm(false);
}
} catch (err) {
console.error('Failed to add item:', err);
}
};
const handleRemoveItem = async (itemId: string) => {
if (!checklist) return;
const updatedItems = checklist.items.filter((item) => item.id !== itemId);
try {
const response = await fetch(`/api/daily-helper/checklists/${checklist.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: updatedItems,
}),
});
if (response.ok) {
const updated = await response.json();
setChecklist(updated);
}
} catch (err) {
console.error('Failed to remove item:', err);
}
};
const handleSaveNotes = async () => {
if (!checklist) return;
try {
const response = await fetch(`/api/daily-helper/checklists/${checklist.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
notes: notes,
}),
});
if (response.ok) {
const updated = await response.json();
setChecklist(updated);
}
} catch (err) {
console.error('Failed to save notes:', err);
}
};
if (loading) {
return (
<div className="bg-gray-900 rounded-lg border border-gray-700 p-6">
<div className="text-center text-gray-400">Loading checklist...</div>
</div>
);
}
if (!checklist) {
return (
<div className="bg-gray-900 rounded-lg border border-gray-700 p-6">
<div className="text-center text-gray-400">No checklist found</div>
</div>
);
}
return (
<div className="bg-gray-900 rounded-lg border border-gray-700 p-6">
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-white capitalize mb-2">
{checklist.checklist_type} Checklist
</h2>
<div className="flex items-center gap-4">
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-400">Completion</span>
<span className="text-sm font-semibold text-white">
{Math.round(checklist.completion_percentage)}%
</span>
</div>
<div className="w-full h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-600 to-blue-400 transition-all duration-300"
style={{ width: `${checklist.completion_percentage}%` }}
/>
</div>
</div>
</div>
</div>
{/* Checklist Items */}
<div className="space-y-2 mb-6">
{checklist.items.map((item) => (
<div
key={item.id}
className="flex items-center gap-3 p-3 bg-gray-800 rounded-lg hover:bg-gray-750 transition-colors group"
>
<button
onClick={() => handleToggleItem(item.id, item.completed)}
className="flex-shrink-0 text-gray-400 hover:text-blue-400 transition-colors"
>
{item.completed ? (
<CheckCircle2 size={24} className="text-green-500" />
) : (
<Circle size={24} />
)}
</button>
<span
className={`flex-1 ${
item.completed
? 'line-through text-gray-500'
: 'text-gray-200'
}`}
>
{item.title}
</span>
<button
onClick={() => handleRemoveItem(item.id)}
className="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-red-400 transition-all"
>
<X size={18} />
</button>
</div>
))}
</div>
{/* Add Item Form */}
{showAddForm ? (
<form onSubmit={handleAddItem} className="mb-6 p-4 bg-gray-800 rounded-lg">
<div className="flex gap-2 mb-3">
<input
type="text"
value={newItemTitle}
onChange={(e) => setNewItemTitle(e.target.value)}
placeholder="New checklist item..."
className="flex-1 bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
autoFocus
/>
</div>
<div className="flex gap-2">
<button
type="submit"
className="flex-1 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded transition-colors"
>
Add
</button>
<button
type="button"
onClick={() => {
setShowAddForm(false);
setNewItemTitle('');
}}
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded transition-colors"
>
Cancel
</button>
</div>
</form>
) : (
<button
onClick={() => setShowAddForm(true)}
className="w-full bg-gray-800 hover:bg-gray-700 text-gray-300 font-medium py-2 px-4 rounded flex items-center justify-center gap-2 transition-colors mb-6"
>
<Plus size={20} />
Add Item
</button>
)}
{/* Notes Section */}
<div className="border-t border-gray-700 pt-6">
<h3 className="text-sm font-semibold text-gray-300 mb-2">Notes</h3>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
onBlur={handleSaveNotes}
placeholder="Add notes for today's trading..."
className="w-full h-24 bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 resize-none"
/>
</div>
{/* Stats */}
<div className="mt-6 grid grid-cols-3 gap-4 p-4 bg-gray-800 rounded-lg">
<div>
<p className="text-xs text-gray-400 mb-1">Total Items</p>
<p className="text-2xl font-bold text-white">{checklist.items.length}</p>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Completed</p>
<p className="text-2xl font-bold text-green-400">
{checklist.items.filter((i) => i.completed).length}
</p>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Remaining</p>
<p className="text-2xl font-bold text-orange-400">
{checklist.items.filter((i) => !i.completed).length}
</p>
</div>
</div>
</div>
);
};
export default DailyChecklistPanel;
+264
View File
@@ -0,0 +1,264 @@
import React, { useState, useEffect } from 'react';
import { Flame, Plus, Trash2, Check } from 'lucide-react';
interface Habit {
id: number;
habit_name: string;
frequency: string;
current_streak: number;
longest_streak: number;
total_completions: number;
created_at: string;
updated_at: string;
}
const HabitTracker: React.FC = () => {
const [habits, setHabits] = useState<Habit[]>([]);
const [loading, setLoading] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const [newHabitName, setNewHabitName] = useState('');
const [newHabitFrequency, setNewHabitFrequency] = useState('daily');
useEffect(() => {
loadHabits();
}, []);
const loadHabits = async () => {
try {
setLoading(true);
const response = await fetch('/api/daily-helper/habits');
if (response.ok) {
const data = await response.json();
setHabits(data);
}
} catch (err) {
console.error('Failed to load habits:', err);
} finally {
setLoading(false);
}
};
const handleAddHabit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newHabitName.trim()) return;
try {
const response = await fetch('/api/daily-helper/habits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
habit_name: newHabitName,
frequency: newHabitFrequency,
}),
});
if (response.ok) {
setNewHabitName('');
setShowAddForm(false);
loadHabits();
}
} catch (err) {
console.error('Failed to create habit:', err);
}
};
const handleLogCompletion = async (habitId: number) => {
try {
const response = await fetch(`/api/daily-helper/habits/${habitId}/log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
habit_id: habitId,
completion_date: new Date().toISOString().split('T')[0],
}),
});
if (response.ok) {
loadHabits();
}
} catch (err) {
console.error('Failed to log completion:', err);
}
};
const handleDeleteHabit = async (habitId: number) => {
if (!confirm('Delete this habit?')) return;
try {
const response = await fetch(`/api/daily-helper/habits/${habitId}`, {
method: 'DELETE',
});
if (response.ok) {
loadHabits();
}
} catch (err) {
console.error('Failed to delete habit:', err);
}
};
const getStreakColor = (streak: number) => {
if (streak >= 30) return 'text-red-400';
if (streak >= 14) return 'text-orange-400';
if (streak >= 7) return 'text-yellow-400';
return 'text-blue-400';
};
const getStreakBadge = (streak: number) => {
if (streak === 0) return null;
if (streak >= 30) return '🔥🔥🔥';
if (streak >= 14) return '🔥🔥';
if (streak >= 7) return '🔥';
return '';
};
return (
<div className="bg-gray-900 rounded-lg border border-gray-700 p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Flame className="text-orange-500" size={28} />
<h2 className="text-2xl font-bold text-white">Habit Tracker</h2>
</div>
<button
onClick={() => setShowAddForm(!showAddForm)}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded flex items-center gap-2 transition-colors"
>
<Plus size={20} />
Add Habit
</button>
</div>
{/* Add Habit Form */}
{showAddForm && (
<form onSubmit={handleAddHabit} className="mb-6 p-4 bg-gray-800 rounded-lg border border-gray-700">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<input
type="text"
value={newHabitName}
onChange={(e) => setNewHabitName(e.target.value)}
placeholder="Habit name (e.g., Daily Planning, Trading Journal)"
className="col-span-2 bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
autoFocus
/>
<select
value={newHabitFrequency}
onChange={(e) => setNewHabitFrequency(e.target.value)}
className="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</div>
<div className="flex gap-2">
<button
type="submit"
className="flex-1 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded transition-colors"
>
Create Habit
</button>
<button
type="button"
onClick={() => setShowAddForm(false)}
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded transition-colors"
>
Cancel
</button>
</div>
</form>
)}
{/* Habits List */}
<div className="space-y-4">
{loading ? (
<div className="text-center text-gray-400 py-8">Loading habits...</div>
) : habits.length === 0 ? (
<div className="text-center text-gray-400 py-8">
<p>No habits yet. Create one to get started!</p>
</div>
) : (
habits.map((habit) => (
<div
key={habit.id}
className="p-4 bg-gray-800 rounded-lg border border-gray-700 hover:border-gray-600 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="text-lg font-semibold text-white mb-2">
{habit.habit_name}
</h3>
<div className="flex flex-wrap gap-6 text-sm">
{/* Current Streak */}
<div>
<p className="text-gray-400 mb-1">Current Streak</p>
<div className="flex items-center gap-2">
<span className={`text-2xl font-bold ${getStreakColor(habit.current_streak)}`}>
{habit.current_streak}
</span>
{getStreakBadge(habit.current_streak) && (
<span className="text-2xl">{getStreakBadge(habit.current_streak)}</span>
)}
</div>
</div>
{/* Longest Streak */}
<div>
<p className="text-gray-400 mb-1">Longest Streak</p>
<p className="text-xl font-bold text-purple-400">
{habit.longest_streak}
</p>
</div>
{/* Total Completions */}
<div>
<p className="text-gray-400 mb-1">Total Completions</p>
<p className="text-xl font-bold text-green-400">
{habit.total_completions}
</p>
</div>
{/* Frequency */}
<div>
<p className="text-gray-400 mb-1">Frequency</p>
<p className="text-sm font-medium text-blue-400 capitalize">
{habit.frequency}
</p>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-2">
<button
onClick={() => handleLogCompletion(habit.id)}
className="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded flex items-center gap-2 transition-colors whitespace-nowrap"
>
<Check size={18} />
Log Today
</button>
<button
onClick={() => handleDeleteHabit(habit.id)}
className="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded flex items-center gap-2 transition-colors"
>
<Trash2 size={18} />
</button>
</div>
</div>
</div>
))
)}
</div>
{/* Motivational Message */}
{habits.length > 0 && (
<div className="mt-6 p-4 bg-blue-900 bg-opacity-30 border border-blue-600 rounded-lg">
<p className="text-blue-200 text-sm">
💡 <strong>Tip:</strong> Consistency is key! Maintain your streaks by completing your habits every day. Even 5 minutes of planning or journaling can transform your trading!
</p>
</div>
)}
</div>
);
};
export default HabitTracker;
@@ -0,0 +1,257 @@
import React, { useState, useEffect } from 'react';
import { Bell, X, Check, ChevronDown } from 'lucide-react';
interface Notification {
id: number;
notification_type: string;
title: string;
message: string;
priority: 'low' | 'normal' | 'high' | 'critical';
read: boolean;
created_at: string;
read_at?: string;
}
interface NotificationListResponse {
notifications: Notification[];
unread_count: number;
total_count: number;
}
const NotificationCenter: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadNotifications();
// Refresh notifications every 30 seconds
const interval = setInterval(loadNotifications, 30000);
return () => clearInterval(interval);
}, []);
const loadNotifications = async () => {
try {
setLoading(true);
const response = await fetch('/api/daily-helper/notifications?limit=10');
if (response.ok) {
const data: NotificationListResponse = await response.json();
setNotifications(data.notifications);
setUnreadCount(data.unread_count);
}
} catch (err) {
console.error('Failed to load notifications:', err);
} finally {
setLoading(false);
}
};
const handleMarkAsRead = async (notificationId: number) => {
try {
const response = await fetch(
`/api/daily-helper/notifications/${notificationId}/read`,
{ method: 'PUT' }
);
if (response.ok) {
loadNotifications();
}
} catch (err) {
console.error('Failed to mark notification as read:', err);
}
};
const handleMarkAllAsRead = async () => {
try {
const response = await fetch(
'/api/daily-helper/notifications/mark-all-read',
{ method: 'POST' }
);
if (response.ok) {
loadNotifications();
}
} catch (err) {
console.error('Failed to mark all as read:', err);
}
};
const handleDelete = async (notificationId: number) => {
try {
const response = await fetch(
`/api/daily-helper/notifications/${notificationId}`,
{ method: 'DELETE' }
);
if (response.ok) {
loadNotifications();
}
} catch (err) {
console.error('Failed to delete notification:', err);
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'critical':
return 'bg-red-900 border-red-600';
case 'high':
return 'bg-orange-900 border-orange-600';
case 'normal':
return 'bg-blue-900 border-blue-600';
case 'low':
return 'bg-gray-800 border-gray-600';
default:
return 'bg-gray-800 border-gray-600';
}
};
const getPriorityDot = (priority: string) => {
switch (priority) {
case 'critical':
return 'bg-red-500';
case 'high':
return 'bg-orange-500';
case 'normal':
return 'bg-blue-500';
case 'low':
return 'bg-gray-500';
default:
return 'bg-gray-500';
}
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
};
return (
<div className="relative">
{/* Notification Bell Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="relative p-2 text-gray-400 hover:text-gray-200 transition-colors"
title="Notifications"
>
<Bell size={24} />
{unreadCount > 0 && (
<span className="absolute top-0 right-0 bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</button>
{/* Notification Dropdown */}
{isOpen && (
<div className="absolute right-0 mt-2 w-96 max-h-[500px] overflow-y-auto bg-gray-900 border border-gray-700 rounded-lg shadow-xl z-50">
{/* Header */}
<div className="sticky top-0 bg-gray-800 border-b border-gray-700 p-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Notifications</h3>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
onClick={handleMarkAllAsRead}
className="text-xs text-blue-400 hover:text-blue-300"
>
Mark all as read
</button>
)}
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-gray-200"
>
<X size={20} />
</button>
</div>
</div>
{/* Notifications List */}
<div className="divide-y divide-gray-700">
{loading ? (
<div className="p-4 text-center text-gray-400">Loading...</div>
) : notifications.length === 0 ? (
<div className="p-8 text-center text-gray-400">
<Bell size={32} className="mx-auto mb-2 opacity-50" />
<p>No notifications yet</p>
</div>
) : (
notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 hover:bg-gray-800 transition-colors ${
!notification.read ? 'bg-gray-800 bg-opacity-50' : ''
}`}
>
<div className="flex gap-3">
{/* Priority Indicator */}
<div
className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${getPriorityDot(
notification.priority
)}`}
/>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<h4 className="font-semibold text-white text-sm line-clamp-2">
{notification.title}
</h4>
<button
onClick={() => handleDelete(notification.id)}
className="text-gray-500 hover:text-gray-300 flex-shrink-0"
>
<X size={16} />
</button>
</div>
<p className="text-sm text-gray-400 mt-1 line-clamp-2">
{notification.message}
</p>
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-gray-500">
{formatTime(notification.created_at)}
</span>
{!notification.read && (
<button
onClick={() => handleMarkAsRead(notification.id)}
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
>
<Check size={14} />
Mark read
</button>
)}
</div>
</div>
</div>
</div>
))
)}
</div>
{/* Footer */}
{notifications.length > 0 && (
<div className="border-t border-gray-700 p-3 bg-gray-800 text-center">
<button
onClick={() => setIsOpen(false)}
className="text-sm text-gray-400 hover:text-gray-200"
>
Close
</button>
</div>
)}
</div>
)}
</div>
);
};
export default NotificationCenter;
@@ -0,0 +1,352 @@
import React, { useState, useEffect } from 'react';
import { Save, X, AlertCircle } from 'lucide-react';
interface UserProfile {
id?: number;
email?: string;
username?: string;
timezone: string;
preferred_trading_start: string;
preferred_trading_end: string;
risk_tolerance: string;
trading_style: string;
daily_target?: number;
max_loss?: number;
notifications_enabled: boolean;
email_reports: boolean;
sms_enabled: boolean;
push_notifications: boolean;
phone_number?: string;
}
interface UserProfileSetupProps {
onClose: () => void;
onSaved?: (profile: UserProfile) => void;
}
const UserProfileSetup: React.FC<UserProfileSetupProps> = ({ onClose, onSaved }) => {
const [profile, setProfile] = useState<UserProfile>({
timezone: 'UTC',
preferred_trading_start: '09:00',
preferred_trading_end: '17:00',
risk_tolerance: 'moderate',
trading_style: 'day_trader',
notifications_enabled: true,
email_reports: true,
sms_enabled: false,
push_notifications: true,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
useEffect(() => {
// Load existing profile
loadProfile();
}, []);
const loadProfile = async () => {
try {
const response = await fetch('/api/daily-helper/profile');
if (response.ok) {
const data = await response.json();
setProfile(data);
}
} catch (err) {
// Profile doesn't exist yet, start fresh
console.log('Starting with default profile');
}
};
const handleChange = (field: keyof UserProfile, value: any) => {
setProfile(prev => ({
...prev,
[field]: value
}));
};
const handleSave = async () => {
setLoading(true);
setError('');
setSuccess(false);
try {
const isUpdate = profile.id;
const method = isUpdate ? 'PUT' : 'POST';
const endpoint = '/api/daily-helper/profile';
const response = await fetch(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profile),
});
if (!response.ok) {
throw new Error('Failed to save profile');
}
const savedProfile = await response.json();
setProfile(savedProfile);
setSuccess(true);
if (onSaved) {
onSaved(savedProfile);
}
setTimeout(() => {
onClose();
}, 1500);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error saving profile');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-gray-700">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">User Profile Setup</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-200"
>
<X size={24} />
</button>
</div>
{error && (
<div className="mb-6 p-4 bg-red-900 bg-opacity-30 border border-red-600 rounded-lg flex items-start gap-3">
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-red-300">{error}</p>
</div>
)}
{success && (
<div className="mb-6 p-4 bg-green-900 bg-opacity-30 border border-green-600 rounded-lg">
<p className="text-green-300"> Profile saved successfully!</p>
</div>
)}
<div className="space-y-6">
{/* Email and Username */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
type="email"
value={profile.email || ''}
onChange={(e) => handleChange('email', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
placeholder="your@email.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Username
</label>
<input
type="text"
value={profile.username || ''}
onChange={(e) => handleChange('username', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
placeholder="your_username"
/>
</div>
</div>
{/* Timezone and Trading Style */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Timezone
</label>
<select
value={profile.timezone}
onChange={(e) => handleChange('timezone', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
>
<option>UTC</option>
<option>EST</option>
<option>CST</option>
<option>MST</option>
<option>PST</option>
<option>GMT</option>
<option>CET</option>
<option>JST</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Trading Style
</label>
<select
value={profile.trading_style}
onChange={(e) => handleChange('trading_style', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
>
<option value="scalper">Scalper (Minutes)</option>
<option value="day_trader">Day Trader (Hours)</option>
<option value="swing_trader">Swing Trader (Days)</option>
</select>
</div>
</div>
{/* Trading Hours */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Trading Start Time (HH:MM)
</label>
<input
type="time"
value={profile.preferred_trading_start}
onChange={(e) => handleChange('preferred_trading_start', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Trading End Time (HH:MM)
</label>
<input
type="time"
value={profile.preferred_trading_end}
onChange={(e) => handleChange('preferred_trading_end', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
/>
</div>
</div>
{/* Risk Profile */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Risk Tolerance
</label>
<select
value={profile.risk_tolerance}
onChange={(e) => handleChange('risk_tolerance', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500"
>
<option value="conservative">Conservative (0.5-1%)</option>
<option value="moderate">Moderate (1-2%)</option>
<option value="aggressive">Aggressive (2-5%)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Daily Max Loss ($)
</label>
<input
type="number"
value={profile.max_loss || ''}
onChange={(e) => handleChange('max_loss', e.target.value ? parseFloat(e.target.value) : null)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
placeholder="500"
/>
</div>
</div>
{/* Daily Target */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Daily Profit Target ($)
</label>
<input
type="number"
value={profile.daily_target || ''}
onChange={(e) => handleChange('daily_target', e.target.value ? parseFloat(e.target.value) : null)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
placeholder="1000"
/>
</div>
{/* Notifications Settings */}
<div className="border-t border-gray-700 pt-6">
<h3 className="text-lg font-semibold text-white mb-4">Notification Preferences</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={profile.push_notifications}
onChange={(e) => handleChange('push_notifications', e.target.checked)}
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
/>
<span className="text-gray-300">Push Notifications</span>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={profile.email_reports}
onChange={(e) => handleChange('email_reports', e.target.checked)}
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
/>
<span className="text-gray-300">Email Reports</span>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={profile.sms_enabled}
onChange={(e) => handleChange('sms_enabled', e.target.checked)}
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
/>
<span className="text-gray-300">SMS Alerts</span>
</label>
{profile.sms_enabled && (
<div className="ml-7">
<input
type="tel"
value={profile.phone_number || ''}
onChange={(e) => handleChange('phone_number', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
placeholder="+1 (555) 123-4567"
/>
</div>
)}
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={profile.notifications_enabled}
onChange={(e) => handleChange('notifications_enabled', e.target.checked)}
className="w-4 h-4 rounded bg-gray-800 border-gray-600"
/>
<span className="text-gray-300">All Notifications Enabled</span>
</label>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-4 mt-8">
<button
onClick={handleSave}
disabled={loading}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white font-medium py-2 px-4 rounded flex items-center justify-center gap-2 transition-colors"
>
<Save size={20} />
{loading ? 'Saving...' : 'Save Profile'}
</button>
<button
onClick={onClose}
disabled={loading}
className="flex-1 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 text-white font-medium py-2 px-4 rounded transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
);
};
export default UserProfileSetup;