dshunt/next18-anniversary-patch
The Next 18 — anniversary-intimacy completion patch Drop-in files that finish the anniversary-intimacy app on branch couple of dolonhunt/romantic-partners-app. They close the three critical gaps found in review: survey/writing answers lost on refresh, the partner reveal never wired into any flow, and no day locking — plus onboarding, games played-tracking, honest media cards, and the Capacitor CLI/core version alignment. Contents File Goes to Action… See the full description on the dataset page: https://huggingface.co/datasets/dshunt/next18-anniversary-patch.
053
1import { motion, AnimatePresence } from 'framer-motion';2import { useState } from 'react';3import { ChevronDown, ChevronUp, CheckCircle2, Lock, Play } from 'lucide-react';4import { courseDays } from '../data/courseData';5 6interface CourseTimelineProps {7 onBeginDay: (day: number) => void;8 onCompleteDay: (day: number) => void;9 completedDays: Set<number>;10}11 12const typeColors: Record<string,string> = {13 game:'from-amber-500/80 to-orange-400/80', survey:'from-blue-500/80 to-cyan-400/80',14 prompt:'from-purple-500/80 to-pink-400/80', learning:'from-emerald-500/80 to-teal-400/80',15 media:'from-rose-500/80 to-red-400/80', reflection:'from-indigo-500/80 to-violet-400/80',16};17const typeLabels: Record<string,string> = { game:'Game', survey:'Survey', prompt:'Writing', learning:'Learning', media:'Media', reflection:'Reflection' };18 19export default function CourseTimeline({ onBeginDay, onCompleteDay, completedDays }: CourseTimelineProps) {20 const [expandedDay, setExpandedDay] = useState<number|null>(null);21 const [week, setWeek] = useState<1|2>(1);22 const days = courseDays.filter(d => week===1 ? d.day<=7 : d.day>7);23 const isLocked = (d: number) => d > 1 && !completedDays.has(d - 1);24 25 return (26 <section id="timeline" className="section-breathe px-4 md:px-8 max-w-5xl mx-auto">27 {/* Header */}28 <motion.div initial={{opacity:0,y:40}} whileInView={{opacity:1,y:0}} viewport={{once:true}} transition={{duration:0.8}}29 className="text-center mb-12 md:mb-20">30 <h2 className="text-3xl md:text-6xl font-light font-display text-gradient-champagne mb-4 md:mb-6 tracking-[0.03em]">Your 14-Day Journey</h2>31 <p className="text-champagne/20 text-sm md:text-base max-w-md mx-auto font-light leading-relaxed tracking-wide">32 Two weeks of escalating exploration — each day unlocks when the previous one is complete33 </p>34 </motion.div>35 36 {/* Week toggle */}37 <div className="flex justify-center mb-10 md:mb-14">38 <div className="glass rounded-full p-1 flex gap-1">39 {([1,2] as const).map(w => (40 <button key={w} onClick={()=>{setWeek(w);setExpandedDay(null)}}41 className={`px-5 md:px-8 py-3 md:py-4 rounded-full text-xs md:text-sm font-medium tracking-[0.1em] uppercase transition-all duration-500 min-w-[110px] md:min-w-[140px] ${42 week===w ? 'btn-primary rounded-full shadow-lg shadow-ember/20' : 'text-champagne/25 hover:text-champagne/40'43 }`}>44 Week {w}45 </button>46 ))}47 </div>48 </div>49 50 {/* Progress */}51 <div className="mb-10 md:mb-14">52 <div className="flex justify-between text-[10px] text-champagne/15 mb-3 tracking-[0.15em] uppercase">53 <span>{completedDays.size} of 14 completed</span>54 <span>{Math.round((completedDays.size/14)*100)}%</span>55 </div>56 <div className="h-1 rounded-full overflow-hidden" style={{background:'rgba(43,27,46,0.3)'}}>57 <motion.div className="h-full rounded-full" style={{background:'linear-gradient(90deg, #9B2335, #B76E79, #E8C9A0)'}}58 initial={{width:0}} animate={{width:`${(completedDays.size/14)*100}%`}} transition={{duration:1.2, ease:[0.23,1,0.32,1]}} />59 </div>60 </div>61 62 {/* Days */}63 <div className="space-y-4 md:space-y-5">64 <AnimatePresence mode="wait">65 {days.map((day, i) => {66 const open = expandedDay===day.day;67 const done = completedDays.has(day.day);68 const locked = isLocked(day.day);69 return (70 <motion.div key={day.day} initial={{opacity:0,y:24}} animate={{opacity:1,y:0}} exit={{opacity:0}}71 transition={{duration:0.4, delay:i*0.05}}72 className={`noir-card overflow-hidden ${locked?'opacity-40':''}`}>73 74 <button className="w-full p-5 md:p-7 text-left flex items-center gap-4 md:gap-5"75 onClick={()=>{ if(!locked) setExpandedDay(open?null:day.day); }}>76 <div className="w-14 h-14 md:w-16 md:h-16 rounded-2xl flex items-center justify-center text-2xl md:text-3xl shrink-0 transition-all duration-500"77 style={done ? {background:'linear-gradient(135deg, #B76E79, #E8C9A0)'}78 : locked ? {background:'rgba(43,27,46,0.3)', border:'1px solid rgba(183,110,121,0.06)'}79 : {background:'rgba(43,27,46,0.3)', border:'1px solid rgba(183,110,121,0.12)'}}>80 {done ? <CheckCircle2 className="w-7 h-7 md:w-8 md:h-8" style={{color:'#2B1B2E'}}/>81 : locked ? <Lock className="w-5 h-5 text-champagne/30"/>82 : day.icon}83 </div>84 85 <div className="flex-1 min-w-0">86 <div className="flex items-center gap-2 mb-1.5">87 <span className="text-[9px] md:text-[10px] font-semibold text-champagne/15 uppercase tracking-[0.18em]">Day {day.day}</span>88 <span className={`px-2 py-0.5 rounded-full text-[8px] md:text-[9px] font-semibold bg-gradient-to-r ${typeColors[day.type]} text-white/80`}>89 {typeLabels[day.type]}90 </span>91 </div>92 <h3 className="text-base md:text-xl font-display font-normal text-champagne/70 mb-0.5">{day.title}</h3>93 <p className="text-[11px] md:text-xs text-champagne/15 font-light">94 {locked ? `🔒 Complete Day ${day.day-1} to unlock` : day.subtitle}95 </p>96 </div>97 98 <div className="shrink-0 text-champagne/10 p-1">99 {locked ? <Lock className="w-4 h-4"/> : open ? <ChevronUp className="w-4 h-4"/> : <ChevronDown className="w-4 h-4" />}100 </div>101 </button>102 103 <AnimatePresence>{open && (104 <motion.div initial={{height:0,opacity:0}} animate={{height:'auto',opacity:1}} exit={{height:0,opacity:0}}105 transition={{duration:0.4}} className="overflow-hidden">106 <div className="px-5 md:px-7 pb-6 md:pb-8 pt-3 border-t" style={{borderColor:'rgba(183,110,121,0.04)'}}>107 <p className="text-xs md:text-sm text-champagne/25 mb-6 leading-[1.8] font-light">{day.description}</p>108 <div className="space-y-3 mb-6">109 {day.content.map((item,j) => (110 <motion.div key={j} initial={{opacity:0,x:-6}} animate={{opacity:1,x:0}} transition={{delay:j*0.04}}111 className="flex items-start gap-3 text-xs md:text-sm">112 <span className="text-rose-gold/20 mt-1 shrink-0 text-[8px]">◆</span>113 <span className="text-champagne/30 font-light">{item}</span>114 </motion.div>115 ))}116 </div>117 {day.tips && (118 <div className="rounded-2xl p-4 md:p-5 mb-5" style={{background:'rgba(74,14,31,0.12)', border:'1px solid rgba(155,35,53,0.06)'}}>119 <p className="text-[9px] font-semibold text-rose-gold/30 uppercase tracking-[0.18em] mb-2">Tips</p>120 {day.tips.map((t,j)=> <p key={j} className="text-[11px] text-champagne/20 mb-1.5 font-light">💡 {t}</p>)}121 </div>122 )}123 <div className="flex gap-3">124 <button onClick={()=>onBeginDay(day.day)}125 className="btn-magnetic btn-primary flex-1 py-3.5 md:py-4 rounded-xl text-xs md:text-sm tracking-[0.08em] uppercase flex items-center justify-center gap-2">126 <Play className="w-3.5 h-3.5"/>{done ? 'Review Day' : 'Begin Day'}127 </button>128 {!done && (129 <button onClick={()=>onCompleteDay(day.day)}130 className="btn-magnetic btn-ghost px-5 py-3.5 md:py-4 rounded-xl text-xs md:text-sm uppercase">131 ✓ Complete132 </button>133 )}134 </div>135 </div>136 </motion.div>137 )}</AnimatePresence>138 </motion.div>139 );140 })}141 </AnimatePresence>142 </div>143 </section>144 );145}