|
| 1 | + |
| 2 | +import React, { useState, useEffect } from 'react'; |
| 3 | +import { useNavigate } from 'react-router-dom'; |
| 4 | +import { motion } from 'framer-motion'; |
| 5 | +import { |
| 6 | + ArrowLeft, |
| 7 | + ChevronRight, |
| 8 | + HelpCircle, |
| 9 | + BookOpen |
| 10 | +} from 'lucide-react'; |
| 11 | +import { Button } from '@/components/ui/button'; |
| 12 | +import { Card, CardContent } from '@/components/ui/card'; |
| 13 | +import { Slider } from '@/components/ui/slider'; |
| 14 | +import { Textarea } from '@/components/ui/textarea'; |
| 15 | +import { Badge } from '@/components/ui/badge'; |
| 16 | +import { useAuth } from '../utils/auth'; |
| 17 | +import { addJournalEntry, getJournalEntries } from '../utils/firebase'; |
| 18 | +import { toast } from 'sonner'; |
| 19 | + |
| 20 | +// List of prompts to rotate through |
| 21 | +const DAILY_PROMPTS = [ |
| 22 | + "What's one thing you're grateful for today?", |
| 23 | + "How did you handle urges or challenges today?", |
| 24 | + "What's something that brought you joy today?", |
| 25 | + "What's something you learned about yourself today?", |
| 26 | + "What's one small victory you had today?", |
| 27 | + "What's something you're looking forward to tomorrow?", |
| 28 | + "What's a way you showed kindness to yourself today?", |
| 29 | + "What's something that challenged you today and how did you respond?", |
| 30 | + "What's one positive change you've noticed in yourself recently?", |
| 31 | + "What did you do for self-care today?", |
| 32 | +]; |
| 33 | + |
| 34 | +// Negative emotions (for lower mood scores) |
| 35 | +const NEGATIVE_EMOTIONS = [ |
| 36 | + "Angry", "Anxious", "Scared", "Overwhelmed", "Ashamed", |
| 37 | + "Disgusted", "Embarrassed", "Frustrated", "Annoyed", |
| 38 | + "Jealous", "Stressed", "Worried", "Guilty", "Surprised", |
| 39 | + "Hopeless", "Irritated", "Lonely", "Discouraged", |
| 40 | + "Disappointed", "Drained", "Sad" |
| 41 | +]; |
| 42 | + |
| 43 | +// Positive emotions (for higher mood scores) |
| 44 | +const POSITIVE_EMOTIONS = [ |
| 45 | + "Amazed", "Excited", "Surprised", "Passionate", "Happy", |
| 46 | + "Joyful", "Brave", "Proud", "Confident", "Hopeful", |
| 47 | + "Amused", "Satisfied", "Relieved", "Grateful", "Content", |
| 48 | + "Calm", "Peaceful", "Inspired", "Loved", "Refreshed" |
| 49 | +]; |
| 50 | + |
| 51 | +// Get mood label based on score |
| 52 | +const getMoodLabel = (score: number): string => { |
| 53 | + if (score <= 2) return "Very Unpleasant"; |
| 54 | + if (score <= 4) return "Slightly Unpleasant"; |
| 55 | + if (score <= 6) return "Neutral"; |
| 56 | + if (score <= 8) return "Slightly Pleasant"; |
| 57 | + return "Very Pleasant"; |
| 58 | +}; |
| 59 | + |
| 60 | +// Get background color based on mood score |
| 61 | +const getMoodBackground = (score: number): string => { |
| 62 | + if (score <= 2) return "bg-slate-800"; |
| 63 | + if (score <= 4) return "bg-slate-700"; |
| 64 | + if (score <= 6) return "bg-slate-600"; |
| 65 | + if (score <= 8) return "bg-emerald-900"; |
| 66 | + return "bg-emerald-800"; |
| 67 | +}; |
| 68 | + |
| 69 | +// Get foreground color based on mood score |
| 70 | +const getMoodColor = (score: number): string => { |
| 71 | + if (score <= 4) return "text-blue-300"; |
| 72 | + if (score <= 6) return "text-slate-200"; |
| 73 | + return "text-green-300"; |
| 74 | +}; |
| 75 | + |
| 76 | +const Journal: React.FC = () => { |
| 77 | + const navigate = useNavigate(); |
| 78 | + const { currentUser } = useAuth(); |
| 79 | + const [step, setStep] = useState(1); |
| 80 | + const [moodScore, setMoodScore] = useState(5); |
| 81 | + const [selectedEmotions, setSelectedEmotions] = useState<string[]>([]); |
| 82 | + const [journalText, setJournalText] = useState(''); |
| 83 | + const [prompt, setPrompt] = useState(''); |
| 84 | + const [isSubmitting, setIsSubmitting] = useState(false); |
| 85 | + |
| 86 | + // Get a random prompt when component mounts |
| 87 | + useEffect(() => { |
| 88 | + const randomIndex = Math.floor(Math.random() * DAILY_PROMPTS.length); |
| 89 | + setPrompt(DAILY_PROMPTS[randomIndex]); |
| 90 | + }, []); |
| 91 | + |
| 92 | + // Get emotions based on mood score |
| 93 | + const relevantEmotions = moodScore <= 5 ? NEGATIVE_EMOTIONS : POSITIVE_EMOTIONS; |
| 94 | + |
| 95 | + const handleEmotionToggle = (emotion: string) => { |
| 96 | + if (selectedEmotions.includes(emotion)) { |
| 97 | + setSelectedEmotions(selectedEmotions.filter(e => e !== emotion)); |
| 98 | + } else { |
| 99 | + // Limit to 3 emotions |
| 100 | + if (selectedEmotions.length < 3) { |
| 101 | + setSelectedEmotions([...selectedEmotions, emotion]); |
| 102 | + } else { |
| 103 | + toast.info("You can select up to 3 emotions"); |
| 104 | + } |
| 105 | + } |
| 106 | + }; |
| 107 | + |
| 108 | + const handleSubmit = async () => { |
| 109 | + if (!currentUser) { |
| 110 | + toast.error("You must be logged in to journal"); |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + if (journalText.trim() === '') { |
| 115 | + toast.error("Please write something in your journal"); |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + setIsSubmitting(true); |
| 120 | + |
| 121 | + try { |
| 122 | + await addJournalEntry({ |
| 123 | + userId: currentUser.uid, |
| 124 | + timestamp: new Date(), |
| 125 | + question: prompt, |
| 126 | + notes: journalText, |
| 127 | + level: moodScore, |
| 128 | + emotions: selectedEmotions |
| 129 | + }); |
| 130 | + |
| 131 | + toast.success("Journal entry saved!"); |
| 132 | + navigate('/dashboard'); |
| 133 | + } catch (error) { |
| 134 | + console.error("Error saving journal entry:", error); |
| 135 | + toast.error("Failed to save journal entry"); |
| 136 | + } finally { |
| 137 | + setIsSubmitting(false); |
| 138 | + } |
| 139 | + }; |
| 140 | + |
| 141 | + const moodLabel = getMoodLabel(moodScore); |
| 142 | + const backgroundClass = getMoodBackground(moodScore); |
| 143 | + const moodColorClass = getMoodColor(moodScore); |
| 144 | + |
| 145 | + return ( |
| 146 | + <div className={`min-h-screen ${backgroundClass} transition-colors duration-300`}> |
| 147 | + <div className="container max-w-md py-8 px-4 mx-auto"> |
| 148 | + <div className="flex justify-between items-center mb-8"> |
| 149 | + <Button |
| 150 | + variant="ghost" |
| 151 | + size="icon" |
| 152 | + className="text-white" |
| 153 | + onClick={() => { |
| 154 | + if (step > 1) { |
| 155 | + setStep(step - 1); |
| 156 | + } else { |
| 157 | + navigate('/dashboard'); |
| 158 | + } |
| 159 | + }} |
| 160 | + > |
| 161 | + <ArrowLeft className="h-6 w-6" /> |
| 162 | + </Button> |
| 163 | + |
| 164 | + <div className={`text-xl font-medium ${moodColorClass}`}> |
| 165 | + {step === 1 ? 'How are you feeling?' : step === 2 ? 'Select emotions' : 'Write in journal'} |
| 166 | + </div> |
| 167 | + |
| 168 | + <Button |
| 169 | + variant="ghost" |
| 170 | + className="text-white" |
| 171 | + disabled={step === 3 ? isSubmitting : false} |
| 172 | + onClick={() => { |
| 173 | + if (step < 3) { |
| 174 | + setStep(step + 1); |
| 175 | + } else { |
| 176 | + handleSubmit(); |
| 177 | + } |
| 178 | + }} |
| 179 | + > |
| 180 | + {step === 3 ? 'Save' : 'Next'} |
| 181 | + {step < 3 && <ChevronRight className="ml-1 h-4 w-4" />} |
| 182 | + </Button> |
| 183 | + </div> |
| 184 | + |
| 185 | + {step === 1 && ( |
| 186 | + <motion.div |
| 187 | + initial={{ opacity: 0 }} |
| 188 | + animate={{ opacity: 1 }} |
| 189 | + exit={{ opacity: 0 }} |
| 190 | + className="space-y-8" |
| 191 | + > |
| 192 | + <div className="text-center"> |
| 193 | + <h1 className="text-3xl font-bold text-white mb-2"> |
| 194 | + Choose how you're feeling |
| 195 | + </h1> |
| 196 | + <p className="text-white/80">right now</p> |
| 197 | + </div> |
| 198 | + |
| 199 | + <div className="flex flex-col items-center justify-center py-10"> |
| 200 | + <div className="relative w-48 h-48 flex items-center justify-center mb-6"> |
| 201 | + <div className={`absolute inset-0 rounded-full blur-lg bg-${moodScore > 5 ? 'green' : 'blue'}-400 opacity-20`}></div> |
| 202 | + <div className={`w-36 h-36 rounded-full ${moodScore > 5 ? 'bg-green-400/30' : 'bg-blue-400/30'} flex items-center justify-center`}> |
| 203 | + <div className={`w-24 h-24 rounded-full ${moodScore > 5 ? 'bg-green-400/50' : 'bg-blue-400/50'} flex items-center justify-center`}> |
| 204 | + <div className={`w-16 h-16 rounded-full ${moodScore > 5 ? 'bg-green-400/70' : 'bg-blue-400/70'} flex items-center justify-center`}> |
| 205 | + <div className={`w-8 h-8 rounded-full ${moodScore > 5 ? 'bg-green-400' : 'bg-blue-400'}`}></div> |
| 206 | + </div> |
| 207 | + </div> |
| 208 | + </div> |
| 209 | + </div> |
| 210 | + |
| 211 | + <h2 className={`text-4xl font-bold mb-12 ${moodColorClass}`}> |
| 212 | + {moodLabel} |
| 213 | + </h2> |
| 214 | + |
| 215 | + <div className="w-full px-4"> |
| 216 | + <Slider |
| 217 | + value={[moodScore]} |
| 218 | + min={1} |
| 219 | + max={10} |
| 220 | + step={1} |
| 221 | + onValueChange={(values) => setMoodScore(values[0])} |
| 222 | + className="my-6" |
| 223 | + /> |
| 224 | + |
| 225 | + <div className="flex justify-between text-white/70 text-sm"> |
| 226 | + <span>VERY UNPLEASANT</span> |
| 227 | + <span>VERY PLEASANT</span> |
| 228 | + </div> |
| 229 | + </div> |
| 230 | + </div> |
| 231 | + </motion.div> |
| 232 | + )} |
| 233 | + |
| 234 | + {step === 2 && ( |
| 235 | + <motion.div |
| 236 | + initial={{ opacity: 0 }} |
| 237 | + animate={{ opacity: 1 }} |
| 238 | + exit={{ opacity: 0 }} |
| 239 | + className="space-y-6" |
| 240 | + > |
| 241 | + <div className="text-center"> |
| 242 | + <h2 className={`text-4xl font-bold mb-2 ${moodColorClass}`}> |
| 243 | + {moodLabel} |
| 244 | + </h2> |
| 245 | + <p className="text-xl text-white/80 mb-8"> |
| 246 | + What best describes this feeling? |
| 247 | + </p> |
| 248 | + <p className="text-sm text-white/60"> |
| 249 | + Select up to 3 emotions |
| 250 | + </p> |
| 251 | + </div> |
| 252 | + |
| 253 | + <div className="flex flex-wrap gap-2 justify-center"> |
| 254 | + {relevantEmotions.map((emotion) => ( |
| 255 | + <Badge |
| 256 | + key={emotion} |
| 257 | + variant={selectedEmotions.includes(emotion) ? "default" : "outline"} |
| 258 | + className={`text-md py-2 px-4 cursor-pointer ${ |
| 259 | + selectedEmotions.includes(emotion) |
| 260 | + ? moodScore > 5 ? 'bg-green-600 hover:bg-green-700' : 'bg-blue-600 hover:bg-blue-700' |
| 261 | + : 'hover:bg-white/10' |
| 262 | + }`} |
| 263 | + onClick={() => handleEmotionToggle(emotion)} |
| 264 | + > |
| 265 | + {emotion} |
| 266 | + </Badge> |
| 267 | + ))} |
| 268 | + </div> |
| 269 | + </motion.div> |
| 270 | + )} |
| 271 | + |
| 272 | + {step === 3 && ( |
| 273 | + <motion.div |
| 274 | + initial={{ opacity: 0 }} |
| 275 | + animate={{ opacity: 1 }} |
| 276 | + exit={{ opacity: 0 }} |
| 277 | + className="space-y-6" |
| 278 | + > |
| 279 | + <Card className="bg-white/10 border-none"> |
| 280 | + <CardContent className="p-4"> |
| 281 | + <h3 className="text-xl font-medium text-white mb-2">Today's Prompt</h3> |
| 282 | + <p className="text-white/80 mb-4">{prompt}</p> |
| 283 | + <Textarea |
| 284 | + value={journalText} |
| 285 | + onChange={(e) => setJournalText(e.target.value)} |
| 286 | + placeholder="Write your thoughts here..." |
| 287 | + className="min-h-[200px] bg-white/5 border-white/20 text-white placeholder:text-white/50" |
| 288 | + /> |
| 289 | + </CardContent> |
| 290 | + </Card> |
| 291 | + |
| 292 | + <div className="space-y-4"> |
| 293 | + <h3 className="text-xl font-medium text-white">Your mood</h3> |
| 294 | + <div className="flex items-center gap-3"> |
| 295 | + <div className={`w-10 h-10 rounded-full ${moodScore > 5 ? 'bg-green-400/70' : 'bg-blue-400/70'} flex-shrink-0`}></div> |
| 296 | + <div> |
| 297 | + <p className={`font-medium ${moodColorClass}`}>{moodLabel}</p> |
| 298 | + <div className="flex flex-wrap gap-1 mt-1"> |
| 299 | + {selectedEmotions.map(emotion => ( |
| 300 | + <Badge key={emotion} variant="outline" className="text-xs bg-white/10"> |
| 301 | + {emotion} |
| 302 | + </Badge> |
| 303 | + ))} |
| 304 | + </div> |
| 305 | + </div> |
| 306 | + </div> |
| 307 | + </div> |
| 308 | + </motion.div> |
| 309 | + )} |
| 310 | + </div> |
| 311 | + </div> |
| 312 | + ); |
| 313 | +}; |
| 314 | + |
| 315 | +export default Journal; |
0 commit comments