Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# See OPENMINDWELL_PROJECT_GUIDE.md for instructions on obtaining these

# Supabase (get from https://app.supabase.com)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
SUPABASE_URL=your-supabase-url-here
SUPABASE_ANON_KEY=your-supabase-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key-here

# HuggingFace (optional - get from https://huggingface.co/settings/tokens)
HUGGINGFACE_API_TOKEN=your-token-here
Expand Down
8 changes: 4 additions & 4 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# Copy this to .env and fill in your actual values

# Supabase Configuration (get from https://app.supabase.com → Project Settings → API)
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_URL=your-supabase-url-here
SUPABASE_ANON_KEY=your-supabase-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key-here

# HuggingFace API Token (optional - get from https://huggingface.co/settings/tokens)
# If not provided, crisis detection will use keyword-based fallback
HUGGINGFACE_API_TOKEN=hf_YourTokenHere
HUGGINGFACE_API_TOKEN=your-huggingface-token-here

# Frontend URL (for CORS whitelist)
FRONTEND_URL=http://localhost:3000
Expand Down
4 changes: 2 additions & 2 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ VITE_API_BASE_URL=http://localhost:3001
VITE_WS_URL=ws://localhost:3001

# Supabase Configuration (same as backend)
VITE_SUPABASE_URL=https://your-project-id.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
VITE_SUPABASE_URL=your-supabase-url-here
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key-here

# Production values:
# VITE_API_BASE_URL=https://your-domain.com
Expand Down
168 changes: 168 additions & 0 deletions frontend/src/components/CheckInModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { useState, useEffect } from 'react';

interface CheckInModalProps {
isOpen: boolean;
habit: {
id: string;
name: string;
} | null;
currentStreak: number;
onClose: () => void;
onCheckIn: (habitId: string, notes?: string) => Promise<void>;
}

const MILESTONES = [7, 30, 100];

export default function CheckInModal({
isOpen,
habit,
currentStreak,
onClose,
onCheckIn
}: CheckInModalProps) {
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [showConfetti, setShowConfetti] = useState(false);
const [milestone, setMilestone] = useState<number | null>(null);

useEffect(() => {
if (!isOpen) {
// Reset state when modal closes
setNotes('');
setSuccess(false);
setShowConfetti(false);
setMilestone(null);
}
}, [isOpen]);

if (!isOpen || !habit) return null;

async function handleCheckIn() {
if (!habit) return; // TypeScript null guard
setLoading(true);

try {
await onCheckIn(habit.id, notes.trim() || undefined);
setSuccess(true);

// Check for milestone
const newStreak = currentStreak + 1;
if (MILESTONES.includes(newStreak)) {
setMilestone(newStreak);
setShowConfetti(true);
}

// Auto-close after delay
setTimeout(() => {
onClose();
}, showConfetti ? 3000 : 1500);
} catch (err) {
console.error('Check-in failed:', err);
} finally {
setLoading(false);
}
}

return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{success ? (
<div className="text-center py-6">
{showConfetti && (
<div className="confetti-container">
{[...Array(50)].map((_, i) => (
<div
key={i}
className="confetti-piece"
style={{
left: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 0.5}s`,
backgroundColor: ['#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff', '#9b59b6'][Math.floor(Math.random() * 5)],
}}
/>
))}
</div>
)}

<div className="text-6xl mb-4 animate-bounce">🎉</div>

{milestone ? (
<>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
🏆 {milestone} Day Streak!
</h2>
<p className="text-gray-600">
Amazing! You've completed {habit.name} for {milestone} days in a row!
</p>
</>
) : (
<>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Great job!
</h2>
<p className="text-gray-600">
You completed {habit.name} today!
</p>
<div className="mt-4 streak-badge inline-flex">
<span className="text-2xl">🔥</span>
<span className="streak-count">{currentStreak + 1}</span>
<span className="streak-label">day streak</span>
</div>
</>
)}
</div>
) : (
<>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-gray-900">Check In</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl leading-none"
>
×
</button>
</div>

<div className="text-center mb-6">
<div className="text-4xl mb-2">✓</div>
<p className="text-lg font-medium text-gray-900">{habit.name}</p>
<p className="text-sm text-gray-500">Current streak: {currentStreak} days</p>
</div>

<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Add a note (optional)
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="How did it go? Any reflections?"
className="input-field resize-none"
rows={3}
/>
</div>

<div className="flex gap-3">
<button
type="button"
onClick={onClose}
className="btn-secondary flex-1"
disabled={loading}
>
Cancel
</button>
<button
onClick={handleCheckIn}
className="btn-primary flex-1"
disabled={loading}
>
{loading ? 'Saving...' : 'Complete ✓'}
</button>
</div>
</>
)}
</div>
</div>
);
}
130 changes: 130 additions & 0 deletions frontend/src/components/CreateHabitModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useState } from 'react';

interface CreateHabitModalProps {
isOpen: boolean;
onClose: () => void;
onCreate: (habit: { name: string; description?: string; frequency: 'daily' | 'weekly' }) => Promise<void>;
}

export default function CreateHabitModal({ isOpen, onClose, onCreate }: CreateHabitModalProps) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [frequency, setFrequency] = useState<'daily' | 'weekly'>('daily');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');

if (!isOpen) return null;

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();

if (!name.trim()) {
setError('Please enter a habit name');
return;
}

setLoading(true);
setError('');

try {
await onCreate({
name: name.trim(),
description: description.trim() || undefined,
frequency,
});

// Reset form
setName('');
setDescription('');
setFrequency('daily');
onClose();
} catch (err: any) {
setError(err.message || 'Failed to create habit');
} finally {
setLoading(false);
}
}

return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-gray-900">Create New Habit</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl leading-none"
>
×
</button>
</div>

<form onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4">
{error}
</div>
)}

<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Habit Name *
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Morning Meditation"
className="input-field"
autoFocus
/>
</div>

<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Description (optional)
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="e.g., 10 minutes of mindfulness"
className="input-field resize-none"
rows={3}
/>
</div>

<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Frequency
</label>
<select
value={frequency}
onChange={(e) => setFrequency(e.target.value as 'daily' | 'weekly')}
className="input-field"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</div>

<div className="flex gap-3">
<button
type="button"
onClick={onClose}
className="btn-secondary flex-1"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="btn-primary flex-1"
disabled={loading}
>
{loading ? 'Creating...' : 'Create Habit'}
</button>
</div>
</form>
</div>
</div>
);
}
Loading