Files
robinhood/frontend/src/components/UserProfileSetup.tsx
T
Claude 7dd2166bf4 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.
2025-11-15 23:09:10 +00:00

353 lines
13 KiB
TypeScript

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;