1const e=`import { useEffect, useState, useCallback } from "react"; 2import { Link } from "react-router-dom"; 3import { CheckCircle2, Circle, ArrowRight, ListChecks } from "lucide-react"; 4import { Card, CardContent } from "@/components/ui/card"; 5import { useAuth } from "@/contexts/AuthContext"; 6import { useProject } from "@/contexts/ProjectContext"; 7import { supabase } from "@/integrations/supabase/client"; 8 9/* ---------- step definitions ---------- */ 10 11interface Step { 12 id: string; 13 label: string; 14 route: string; 15 /** returns true if step is complete */ 16 check: (userId: string, projectId?: string) => Promise<boolean>; 17} 18 19const STEPS: Step[] = [ 20 { 21 id: "builder-profile", 22 label: "Complete Builder Profile", 23 route: "/builder-profile", 24 check: async (uid) => { 25 const { data } = await supabase 26 .from("builder_profiles") 27 .select("id") 28 .eq("user_id", uid) 29 .eq("archived", false) 30 .limit(1) 31 .maybeSingle(); 32 return !!data; 33 }, 34 }, 35 { 36 id: "scrap-or-scale", 37 label: "Score your best idea with Idea Scorecard", 38 route: "/intake", 39 check: async (uid, projectId) => { 40 let q = supabase.from("scores").select("id", { count: "exact", head: true }).eq("user_id", uid); 41 if (projectId) q = q.eq("project_id", projectId); 42 const { count } = await q; 43 return (count ?? 0) > 0; 44 }, 45 }, 46 { 47 id: "icp-builder", 48 label: "Build your ICP with the ICP Builder", 49 route: "/icp-builder", 50 check: async (uid, projectId) => { 51 let q = supabase.from("icp_profiles").select("id", { count: "exact", head: true }).eq("user_id", uid); 52 if (projectId) q = q.eq("project_id", projectId); 53 const { count } = await q; 54 return (count ?? 0) > 0; 55 }, 56 }, 57 { 58 id: "the-jury", 59 label: "Test your concept with The Jury", 60 route: "/jury", 61 check: async (uid, projectId) => { 62 let q = supabase.from("jury_sessions").select("id", { count: "exact", head: true }).eq("user_id", uid); 63 if (projectId) q = q.eq("project_id", projectId); 64 const { count } = await q; 65 return (count ?? 0) > 0; 66 }, 67 }, 68 { 69 id: "product-generator", 70 label: "Build your product with the GI Product Generator (Pro)", 71 route: "/product-generator", 72 check: async (uid, projectId) => { 73 let q = supabase.from("gi_product_projects").select("id", { count: "exact", head: true }).eq("user_id", uid).eq("status", "complete"); 74 if (projectId) q = q.eq("workspace_project_id", projectId); 75 const { count } = await q; 76 return (count ?? 0) > 0; 77 }, 78 }, 79 { 80 id: "offer-architect", 81 label: "Design your offer with Offer Architect", 82 route: "/offer-architect", 83 check: async (uid, projectId) => { 84 let q = supabase.from("offer_architectures").select("id", { count: "exact", head: true }).eq("user_id", uid); 85 if (projectId) q = q.eq("project_id", projectId); 86 const { count } = await q; 87 return (count ?? 0) > 0; 88 }, 89 }, 90 { 91 id: "ai-fluency", 92 label: "Start AI Fluency (Day 1 of 36)", 93 route: "/academy/ai-fluency", 94 check: async (uid) => { 95 const { data } = await supabase 96 .from("user_lab_progress") 97 .select("prompts_completed") 98 .eq("user_id", uid) 99 .maybeSingle(); 100 return (data?.prompts_completed ?? 0) > 0; 101 }, 102 }, 103 { 104 id: "ops-assessment", 105 label: "Run your Operations Assessment", 106 route: "/operations", 107 check: async (uid) => { 108 const { count } = await supabase 109 .from("operations_assessments") 110 .select("id", { count: "exact", head: true }) 111 .eq("user_id", uid); 112 return (count ?? 0) > 0; 113 }, 114 }, 115 { 116 id: "content-accelerator", 117 label: "Set up your content system with the Accelerator", 118 route: "/accelerator", 119 check: async (uid, projectId) => { 120 let q = supabase.from("accelerator_profiles").select("id", { count: "exact", head: true }).eq("user_id", uid).eq("setup_complete", true); 121 if (projectId) q = q.eq("project_id", projectId); 122 const { count } = await q; 123 return (count ?? 0) > 0; 124 }, 125 }, 126 { 127 id: "sop-builder", 128 label: "Build your first SOP", 129 route: "/sop-builder", 130 check: async (uid, projectId) => { 131 let q = supabase.from("sop_documents").select("id", { count: "exact", head: true }).eq("user_id", uid); 132 if (projectId) q = q.eq("project_id", projectId); 133 const { count } = await q; 134 return (count ?? 0) > 0; 135 }, 136 }, 137 { 138 id: "coaching", 139 label: "Explore Revenue Systems Coaching", 140 route: "/sprint", 141 check: async (uid) => { 142 const { count } = await supabase 143 .from("coaching_applications") 144 .select("id", { count: "exact", head: true }) 145 .eq("user_id", uid); 146 return (count ?? 0) > 0; 147 }, 148 }, 149]; 150/* ---------- component ---------- */ 151 152export function ActionPlanChecklist() { 153 const { user } = useAuth(); 154 const { activeProject } = useProject(); 155 const [statuses, setStatuses] = useState<Record<string, boolean>>({}); 156 const [loading, setLoading] = useState(true); 157 158 const refresh = useCallback(async () => { 159 if (!user) return; 160 const pid = activeProject?.id; 161 const results = await Promise.all( 162 STEPS.map(async (s) => { 163 const done = await s.check(user.id, pid); 164 return [s.id, done] as [string, boolean]; 165 }) 166 ); 167 setStatuses(Object.fromEntries(results));
168 setLoading(false); 169 }, [user, activeProject?.id]); 170 // Initial load 171 useEffect(() => { 172 refresh(); 173 }, [refresh]); 174 175 // Real-time subscription â listen for inserts on key tables 176 useEffect(() => { 177 if (!user) return; 178 179 const tables = [ 180 "builder_profiles", 181 "projects", 182 "icp_profiles", 183 "jury_sessions", 184 "offer_architectures", 185 "gi_product_projects", 186 "user_lab_progress", 187 "operations_assessments", 188 "accelerator_profiles", 189 "sop_documents", 190 "coaching_applications", 191 ]; 192 193 const channel = supabase 194 .channel("action-plan-realtime") 195 .on( 196 "postgres_changes", 197 { 198 event: "INSERT", 199 schema: "public", 200 filter: \`user_id=eq.\${user.id}\`, 201 }, 202 () => { 203 // Refresh all statuses on any insert 204 refresh(); 205 } 206 ) 207 .subscribe(); 208 209 return () => { 210 supabase.removeChannel(channel); 211 }; 212 }, [user, refresh]); 213 214 // Also refresh when window regains focus (covers navigating back) 215 useEffect(() => { 216 const handler = () => refresh(); 217 window.addEventListener("focus", handler); 218 return () => window.removeEventListener("focus", handler); 219 }, [refresh]); 220 221 if (!user) return null; 222 223 const completedCount = Object.values(statuses).filter(Boolean).length; 224 const totalSteps = STEPS.length; 225 const allDone = completedCount === totalSteps; 226 227 // Find the first incomplete step for the "next" highlight 228 const nextStep = STEPS.find((s) => !statuses[s.id]); 229 230 // Get archetype label for personalization 231 const archetypeFromStatuses = statuses["builder-profile"]; 232 233 return ( 234 <Card className="border-border/70 bg-card/80 border-l-[3px] border-l-accent"> 235 <CardContent className="p-5 sm:p-6"> 236 <div className="flex items-center gap-2 mb-1"> 237 <ListChecks className="h-4 w-4 text-accent" /> 238 <h3 className="font-satoshi text-sm font-bold text-foreground"> 239 Your Action Plan 240 </h3> 241 <span className="ml-auto text-xs font-medium text-muted-foreground"> 242 {loading ? "â¦" : \`\${completedCount}/\${totalSteps}\`} 243 </span> 244 </div> 245 246 {/* Progress bar */} 247 <div className="h-1.5 w-full rounded-full bg-secondary/60 mb-4 mt-2 overflow-hidden"> 248 <div 249 className="h-full rounded-full bg-accent transition-all duration-700 ease-out" 250 style={{ width: \`\${(completedCount / totalSteps) * 100}%\` }} 251 /> 252 </div> 253 254 {allDone && ( 255 <p className="text-xs text-accent font-semibold mb-3"> 256 ð You've completed every step. You're in the top tier of builders. 257 </p> 258 )} 259 260 <ul className="space-y-1.5"> 261 {STEPS.map((step) => { 262 const done = statuses[step.id]; 263 const isNext = nextStep?.id === step.id; 264 return ( 265 <li key={step.id}> 266 <Link 267 to={step.route} 268 className={\`group flex items-start gap-2.5 rounded-md px-2 py-1.5 transition-colors \${ 269 isNext && !done 270 ? "bg-accent/5 border border-accent/20" 271 : "hover:bg-secondary/40" 272 }\`} 273 > 274 {done ? ( 275 <CheckCircle2 className="h-4 w-4 text-accent shrink-0 mt-0.5" /> 276 ) : ( 277 <Circle 278 className={\`h-4 w-4 shrink-0 mt-0.5 \${ 279 isNext 280 ? "text-accent" 281 : "text-muted-foreground/40" 282 }\`} 283 /> 284 )} 285 <span 286 className={\`text-xs leading-snug \${ 287 done 288 ? "text-muted-foreground line-through" 289 : isNext 290 ? "text-foreground font-semibold" 291 : "text-muted-foreground" 292 }\`} 293 > 294 {done && step.id === "builder-profile" 295 ? "Complete Builder Profile â Done" 296 : step.label} 297 </span> 298 {isNext && !done && ( 299 <ArrowRight className="h-3 w-3 text-accent ml-auto shrink-0 mt-0.5 opacity-0 group-hover:opacity-100 transition-opacity" /> 300 )} 301 </Link> 302 </li> 303 ); 304 })} 305 </ul> 306 </CardContent> 307 </Card> 308 ); 309} 310`;export{e as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.