AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Trophy, Calendar, TrendingUp, ChevronRight, Flag, PartyPopper } from 'lucide-react'
|
||||
import { format, addDays, differenceInDays } from 'date-fns'
|
||||
|
||||
import { Card, Button } from '@/components/ui'
|
||||
import { showToast } from '@/components/ui'
|
||||
|
||||
interface Milestone {
|
||||
id: string
|
||||
cycleNumber: number
|
||||
date: string
|
||||
status: 'UPCOMING' | 'COMPLETED' | 'SKIPPED'
|
||||
notes?: string
|
||||
celebratedAt?: string
|
||||
}
|
||||
|
||||
interface TreatmentPlan {
|
||||
id: string
|
||||
title: string
|
||||
totalCycles: number
|
||||
currentCycle: number
|
||||
startDate: string | null
|
||||
estimatedEnd: string | null
|
||||
status: 'ACTIVE' | 'PAUSED' | 'COMPLETED'
|
||||
cycleType: string
|
||||
cycleDays: number
|
||||
milestones: Milestone[]
|
||||
}
|
||||
|
||||
interface TreatmentProgressProps {
|
||||
plan: TreatmentPlan | null
|
||||
workspaceId: string
|
||||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
export function TreatmentProgress({ plan, workspaceId, onUpdate }: TreatmentProgressProps) {
|
||||
const [showCelebration, setShowCelebration] = useState(false)
|
||||
const [celebratingMilestone, setCelebratingMilestone] = useState<Milestone | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Check for uncelebrated completed milestones
|
||||
if (plan?.milestones) {
|
||||
const uncelebrated = plan.milestones.find(
|
||||
m => m.status === 'COMPLETED' && !m.celebratedAt
|
||||
)
|
||||
if (uncelebrated) {
|
||||
setCelebratingMilestone(uncelebrated)
|
||||
setShowCelebration(true)
|
||||
// Mark as celebrated
|
||||
markCelebrated(uncelebrated.id)
|
||||
}
|
||||
}
|
||||
}, [plan])
|
||||
|
||||
const markCelebrated = async (milestoneId: string) => {
|
||||
try {
|
||||
await fetch(`/api/workspaces/${workspaceId}/treatment-plan/milestones/${milestoneId}/celebrate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to mark celebration:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCompleteCycle = async () => {
|
||||
if (!plan) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workspaces/${workspaceId}/treatment-plan/complete-cycle`, {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Failed to complete cycle')
|
||||
|
||||
showToast('Cycle completed! Great job!', 'success')
|
||||
onUpdate?.()
|
||||
} catch {
|
||||
showToast('Failed to complete cycle', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<Card variant="outline" className="text-center py-8">
|
||||
<Trophy className="w-12 h-12 text-secondary-300 mx-auto mb-3" />
|
||||
<h3 className="font-semibold text-secondary-900 mb-1">No Treatment Plan</h3>
|
||||
<p className="text-secondary-500 text-sm mb-4">
|
||||
Set up a treatment plan to track your progress
|
||||
</p>
|
||||
<Button href={`/treatment/new`}>Create Plan</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const progress = (plan.currentCycle / plan.totalCycles) * 100
|
||||
const daysRemaining = plan.estimatedEnd
|
||||
? differenceInDays(new Date(plan.estimatedEnd), new Date())
|
||||
: null
|
||||
|
||||
const milestones = plan.milestones || []
|
||||
const completedCount = milestones.filter(m => m.status === 'COMPLETED').length
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-primary-500 to-primary-600 text-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{plan.title}</h3>
|
||||
<p className="text-primary-100 text-sm">
|
||||
Cycle {plan.currentCycle} of {plan.totalCycles}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<Trophy className="w-7 h-7" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="text-secondary-600">{completedCount} completed</span>
|
||||
<span className="font-semibold text-primary-600">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<div className="h-3 bg-secondary-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-primary-500 to-primary-400 transition-all duration-500"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-border">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-secondary-900">{plan.currentCycle}</p>
|
||||
<p className="text-xs text-secondary-500">Current</p>
|
||||
</div>
|
||||
<div className="text-center border-x border-border">
|
||||
<p className="text-2xl font-bold text-secondary-900">{plan.totalCycles - plan.currentCycle}</p>
|
||||
<p className="text-xs text-secondary-500">Remaining</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-secondary-900">
|
||||
{daysRemaining !== null ? Math.max(0, daysRemaining) : '?'}
|
||||
</p>
|
||||
<p className="text-xs text-secondary-500">Days Left</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="px-4 pb-4 space-y-2">
|
||||
{plan.status === 'ACTIVE' && plan.currentCycle < plan.totalCycles && (
|
||||
<Button
|
||||
onClick={handleCompleteCycle}
|
||||
fullWidth
|
||||
className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700"
|
||||
>
|
||||
<PartyPopper className="w-4 h-4 mr-2" />
|
||||
Complete Cycle {plan.currentCycle + 1}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="/treatment/details"
|
||||
fullWidth
|
||||
>
|
||||
View Details
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Milestone Preview */}
|
||||
{milestones.length > 0 && (
|
||||
<Card className="mt-4">
|
||||
<h4 className="font-semibold text-secondary-900 mb-3">Upcoming Milestones</h4>
|
||||
<div className="space-y-2">
|
||||
{milestones
|
||||
.filter(m => m.status === 'UPCOMING')
|
||||
.slice(0, 3)
|
||||
.map(milestone => (
|
||||
<div
|
||||
key={milestone.id}
|
||||
className="flex items-center gap-3 p-2 rounded-lg bg-secondary-50"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
|
||||
<Flag className="w-4 h-4 text-primary-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-secondary-900">
|
||||
Cycle {milestone.cycleNumber}
|
||||
</p>
|
||||
<p className="text-xs text-secondary-500">
|
||||
{format(new Date(milestone.date), 'MMM d, yyyy')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Celebration Modal */}
|
||||
{showCelebration && celebratingMilestone && (
|
||||
<MilestoneCelebration
|
||||
milestone={celebratingMilestone}
|
||||
plan={plan}
|
||||
onClose={() => setShowCelebration(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface MilestoneCelebrationProps {
|
||||
milestone: Milestone
|
||||
plan: TreatmentPlan
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function MilestoneCelebration({ milestone, plan, onClose }: MilestoneCelebrationProps) {
|
||||
const messages = [
|
||||
"You're doing amazing!",
|
||||
"One step closer to the finish line!",
|
||||
"Your strength is inspiring!",
|
||||
"Keep going, you've got this!",
|
||||
"Another milestone conquered!"
|
||||
]
|
||||
|
||||
const randomMessage = messages[Math.floor(Math.random() * messages.length)]
|
||||
|
||||
const isHalfway = milestone.cycleNumber === Math.floor(plan.totalCycles / 2)
|
||||
const isFinal = milestone.cycleNumber === plan.totalCycles
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl p-6 max-w-sm w-full text-center animate-in fade-in zoom-in duration-300">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-r from-yellow-400 to-orange-500 flex items-center justify-center mx-auto mb-4 animate-bounce">
|
||||
<PartyPopper className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-secondary-900 mb-2">
|
||||
{isFinal ? '🎉 Treatment Complete!' : isHalfway ? '🌟 Halfway There!' : 'Milestone Reached!'}
|
||||
</h2>
|
||||
|
||||
<p className="text-secondary-600 mb-2">
|
||||
Cycle {milestone.cycleNumber} of {plan.totalCycles} completed!
|
||||
</p>
|
||||
|
||||
<p className="text-primary-600 font-medium mb-6">
|
||||
{isFinal ? "You've done it! What an incredible journey." : randomMessage}
|
||||
</p>
|
||||
|
||||
<Button onClick={onClose} fullWidth>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user