PageSourceSearch

https://www.generative-income.ai/assets/ActionPlanChecklist-DpT47Jnr.js

js generative-income.ai collected 2026-10-05 12:17:15 UTC 10,075 bytes, 310 lines download raw bytes

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.