File size: 18,526 Bytes
aaa634c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { ExternalLink, Search, CheckCircle, XCircle, ArrowLeft, RefreshCw, Star, Info } from 'lucide-react';

const JOURNEY_SEQUENCE = [
  217, 242, 1, 49, 347, 271, 238, 36, 128, 125, 167, 15, 11, 42, 121, 3, 424, 567, 76, 239, 20, 
  155, 150, 739, 853, 84, 704, 74, 875, 153, 33, 981, 4, 206, 21, 141, 143, 19, 138, 2, 287, 
  146, 23, 25, 226, 104, 543, 110, 100, 572, 235, 102, 199, 1448, 98, 230, 105, 124, 297, 703, 
  1046, 973, 215, 621, 355, 295, 78, 39, 40, 46, 90, 22, 79, 131, 17, 51, 208, 211, 212, 200, 
  695, 133, 286, 994, 417, 130, 207, 210, 261, 323, 684, 127, 743, 332, 1584, 778, 269, 787, 
  70, 746, 198, 213, 5, 647, 91, 322, 152, 139, 300, 416, 62, 1143, 309, 518, 494, 97, 329, 
  115, 72, 312, 10, 53, 55, 45, 134, 846, 1899, 763, 678, 57, 56, 435, 252, 253, 1851, 48, 
  54, 73, 202, 66, 50, 43, 2013, 136, 191, 338, 190, 268, 371, 7
];

interface JourneyPathProps {
  onSelectProblemForReview: (problemId: string) => void;
}

export default function JourneyPath({ onSelectProblemForReview }: JourneyPathProps) {
  const [completions, setCompletions] = useState<Set<number>>(new Set());
  const [titles, setTitles] = useState<Record<number, string>>({});
  const [loading, setLoading] = useState(true);
  const [selectedNum, setSelectedNum] = useState<number | null>(null);
  const [details, setDetails] = useState<any[]>([]);
  const [modalLoading, setModalLoading] = useState(false);
  const [toggleLoading, setToggleLoading] = useState(false);

  const fetchData = async () => {
    try {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) return;

      const headers = { 'Authorization': `Bearer ${session.access_token}` };
      
      const [progRes, titlesRes] = await Promise.all([
        fetch(`${import.meta.env.VITE_API_BASE_URL}/api/journey-progress`, { headers }),
        fetch(`${import.meta.env.VITE_API_BASE_URL}/api/journey-progress/titles`, { headers })
      ]);

      if (progRes.ok && titlesRes.ok) {
        const progData = await progRes.json();
        const titlesData = await titlesRes.json();
        setCompletions(new Set(progData));
        setTitles(titlesData);
      }
    } catch (err) {
      console.error('Error fetching journey data:', err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  const handleNodeClick = async (num: number) => {
    setSelectedNum(num);
    setDetails([]);
    setModalLoading(true);
    try {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) return;

      const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/reviews/problems/${num}`, {
        headers: { 'Authorization': `Bearer ${session.access_token}` }
      });
      if (res.ok) {
        const data = await res.json();
        setDetails(data);
      }
    } catch (err) {
      console.error('Error fetching problem details:', err);
    } finally {
      setModalLoading(false);
    }
  };

  const handleToggleCompletion = async (num: number) => {
    setToggleLoading(true);
    try {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) return;

      const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/journey-progress/toggle`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${session.access_token}`
        },
        body: JSON.stringify({ problem_number: num })
      });

      if (res.ok) {
        const updated = new Set(completions);
        if (updated.has(num)) {
          updated.delete(num);
        } else {
          updated.add(num);
        }
        setCompletions(updated);
      }
    } catch (err) {
      console.error('Error toggling progress:', err);
    } finally {
      setToggleLoading(false);
    }
  };

  // Compute stats
  const total = JOURNEY_SEQUENCE.length;
  const completedCount = JOURNEY_SEQUENCE.filter(num => completions.has(num)).length;
  const percentage = total > 0 ? Math.round((completedCount / total) * 100) : 0;

  if (loading) {
    return (
      <div className="w-full h-full flex items-center justify-center bg-[#fdf8e1] text-ink-black">
        <div className="flex flex-col items-center gap-3">
          <RefreshCw className="w-8 h-8 text-primary animate-spin" />
          <span className="font-arcade text-[10px] tracking-wide">COMPILING MAP...</span>
        </div>
      </div>
    );
  }

  // Determine active node: first node in sequence that is not completed
  const activeNode = JOURNEY_SEQUENCE.find(num => !completions.has(num)) || JOURNEY_SEQUENCE[0];

  return (
    <div className="w-full h-full bg-[#fdf8e1] p-6 overflow-y-auto custom-scrollbar flex flex-col relative">
      <div className="flex flex-col gap-4">
        {/* Header progress info */}
        <div className="flex flex-col md:flex-row items-center justify-between p-4 bg-white border-[3px] border-ink-black shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] gap-4 select-none">
          <div>
            <h3 className="font-headline-md font-bold text-ink-black text-sm m-0">MAP PROGRESS STATUS</h3>
            <span className="text-[10px] font-arcade text-trash-gray font-bold uppercase mt-1 block">Leitner path completion metrics</span>
          </div>
          <div className="flex items-center gap-4">
            <span className="font-arcade text-[10px] font-bold text-ink-black">{percentage}% DONE</span>
            <div className="w-48 h-6 bg-slate-100 border-[3px] border-ink-black relative overflow-hidden">
              <div 
                className="absolute inset-y-0 left-0 bg-[#0ea5e9] border-r-[3px] border-ink-black transition-all duration-500"
                style={{ width: `${percentage}%` }}
              />
            </div>
          </div>
        </div>

        {/* Milestone path visualization */}
        <div className="bg-white border-[3px] border-ink-black shadow-[4px_4px_0px_0px_rgba(30,41,59,1)] relative p-8 mt-4 overflow-hidden min-h-[700px] flex flex-col items-center">
          {/* Timeline Center Sine Path via SVG */}
          <svg className="absolute inset-y-0 w-full h-full pointer-events-none" preserveAspectRatio="none" viewBox="0 0 100 1000">
            <path 
              d="M 50,0 Q 70,50 30,100 T 50,200 T 70,300 T 30,400 T 50,500 T 70,600 T 30,700 T 50,800 T 70,900 T 50,1000" 
              fill="none" 
              opacity="0.3" 
              stroke="#1E293B" 
              strokeDasharray="5,15" 
              strokeLinecap="round" 
              strokeWidth="6"
            />
          </svg>

          {/* Nodes Container */}
          <div className="relative z-10 flex flex-col items-center gap-10 w-full max-w-sm">
            {JOURNEY_SEQUENCE.map((num, index) => {
              const isCompleted = completions.has(num);
              const isSynced = num in titles;
              const title = titles[num] || 'Unsynced Note';
              const isActive = num === activeNode;
              
              // Sine wave displacement
              const xOffset = Math.sin(index * 0.8) * 75;

              return (
                <div
                  key={num}
                  className="relative group flex items-center justify-center"
                  style={{ transform: `translateX(${xOffset}px)` }}
                >
                  {/* Tooltip */}
                  <div className="absolute bottom-[115%] left-1/2 -translate-x-1/2 bg-paper-white text-ink-black border-[3px] border-ink-black text-[9px] font-arcade px-3 py-2 rounded-xl whitespace-nowrap shadow-[4px_4px_0px_0px_#1E293B] opacity-0 group-hover:opacity-100 transition-all duration-200 scale-90 group-hover:scale-100 pointer-events-none z-30">
                    <span className="font-bold text-primary mr-1">#{num}</span>
                    <span>{title.toUpperCase()}</span>
                    {!isSynced && <span className="ml-2 text-red-500 font-bold">(LOCKED)</span>}
                  </div>

                  {/* Circular / Rounded Square Node */}
                  <button
                    onClick={() => handleNodeClick(num)}
                    className={`w-14 h-14 border-[3px] border-ink-black flex items-center justify-center font-arcade font-bold text-xs cursor-pointer transition-all duration-200 hover:scale-110 active:scale-95 ${
                      isCompleted
                        ? 'bg-grass-green text-ink-black shadow-[3px_3px_0px_0px_#1E293B] rounded-lg'
                        : isActive
                        ? 'bg-[#ffe24c] text-ink-black shadow-[4px_4px_0px_0px_#F472B6] rounded-xl animate-bounce border-dashed'
                        : isSynced
                        ? 'bg-[#c0e8ff] text-ink-black shadow-[3px_3px_0px_0px_#1E293B] rounded-lg'
                        : 'bg-slate-200 text-trash-gray opacity-60 shadow-[1px_1px_0px_0px_#1E293B] rounded-lg'
                    }`}
                  >
                    {isCompleted ? '✓' : isActive ? '▶' : num}
                  </button>
                </div>
              );
            })}
          </div>
        </div>
      </div>

      {/* Modal Dialog (Styled as a Retro OS System Dialog) */}
      {selectedNum !== null && (
        <div className="fixed inset-0 bg-black/50 backdrop-blur-xs flex items-center justify-center z-[100] p-4 animate-fade-in">
          <div className="w-full max-w-md bg-paper-white border-[4px] border-ink-black p-0 shadow-[8px_8px_0px_0px_rgba(30,41,59,1)] relative select-none">
            {/* Title Bar */}
            <div className="bg-[#0ea5e9] border-b-[4px] border-ink-black px-4 py-2 flex justify-between items-center text-white font-bold text-sm">
              <div className="flex items-center gap-2">
                <span className="material-symbols-outlined text-sm font-bold">info</span>
                <span>Problem_Detail.sys</span>
              </div>
              <button
                onClick={() => setSelectedNum(null)}
                className="w-6 h-6 bg-highlight-pink text-white border-2 border-ink-black flex items-center justify-center hover:bg-rose-600 active:translate-y-0.5 shadow-[1px_1px_0px_0px_rgba(30,41,59,1)] active:shadow-none transition-all cursor-pointer font-bold"
              >
                &times;
              </button>
            </div>

            {/* Dialog Content */}
            <div className="p-6 bg-[#fdf8e1] space-y-4">
              <div className="pb-3 border-b-2 border-dashed border-ink-black">
                <span className="text-[8px] font-arcade font-bold text-trash-gray uppercase block">Milestone Index</span>
                <h3 className="text-xl font-bold font-headline-md text-ink-black mt-1">LeetCode Problem #{selectedNum}</h3>
              </div>

              {modalLoading ? (
                <div className="py-8 flex flex-col items-center justify-center gap-3">
                  <RefreshCw className="w-6 h-6 text-primary animate-spin" />
                  <span className="text-[8px] font-arcade font-bold text-trash-gray uppercase">FETCHING DATA...</span>
                </div>
              ) : details.length > 0 ? (
                <div className="space-y-4">
                  {details.map((prob) => (
                    <div key={prob.id} className="p-4 bg-white border-[3px] border-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)] space-y-3">
                      <div className="flex justify-between items-start gap-2">
                        <div>
                          <h4 className="font-bold text-ink-black text-sm font-headline-md leading-tight m-0">{prob.name}</h4>
                          <span className="text-[8px] font-arcade text-trash-gray font-bold uppercase mt-1 block">Pattern: {prob.pattern}</span>
                        </div>
                        <span className={`text-[8px] font-arcade font-bold px-2 py-0.5 border-2 border-ink-black ${
                          prob.difficulty === 'Easy' ? 'bg-grass-green text-ink-black' :
                          prob.difficulty === 'Medium' ? 'bg-secondary-container text-ink-black' :
                          'bg-highlight-pink text-white'
                        }`}>
                          {prob.difficulty.toUpperCase()}
                        </span>
                      </div>

                      <div className="flex gap-4 text-xs font-bold text-ink-black">
                        <div>Time: <code className="bg-[#1E293B] text-[#4ADE80] font-code-mono px-1.5 py-0.5 border border-ink-black">{prob.optimal_time}</code></div>
                        <div>Space: <code className="bg-[#1E293B] text-[#4ADE80] font-code-mono px-1.5 py-0.5 border border-ink-black">{prob.optimal_space}</code></div>
                      </div>

                      <div className="flex flex-col sm:flex-row gap-2 pt-2 border-t border-slate-200">
                        <a
                          href={prob.google_doc_url}
                          target="_blank"
                          rel="noreferrer"
                          className="flex-1 flex items-center justify-center gap-1.5 py-2 border-[3px] border-ink-black bg-white hover:bg-slate-100 text-xs font-bold text-ink-black shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer text-center"
                        >
                          <ExternalLink className="w-3.5 h-3.5" />
                          Google Doc
                        </a>
                        <button
                          onClick={() => {
                            onSelectProblemForReview(prob.id);
                            setSelectedNum(null);
                          }}
                          className="flex-1 flex items-center justify-center gap-1.5 py-2 border-[3px] border-ink-black bg-[#ffe24c] hover:bg-yellow-300 text-xs font-bold text-ink-black shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer text-center"
                        >
                          <Star className="w-3.5 h-3.5" />
                          Review Problem
                        </button>
                      </div>
                    </div>
                  ))}
                </div>
              ) : (
                <div className="p-4 bg-white border-[3px] border-ink-black shadow-[3px_3px_0px_0px_rgba(30,41,59,1)] space-y-4">
                  <div className="flex items-start gap-3 text-xs font-bold text-ink-black leading-relaxed">
                    <Info className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
                    <div>
                      <h4 className="font-bold text-ink-black m-0">No Local Note Synced</h4>
                      <p className="text-[10px] text-trash-gray mt-1 leading-normal font-semibold">
                        You haven't synced notes for LeetCode #{selectedNum} yet. Click "Sync Google Drive" to import your progress notes.
                      </p>
                    </div>
                  </div>

                  <div className="space-y-2 pt-2 border-t border-slate-200">
                    <span className="text-[8px] font-arcade text-trash-gray uppercase font-bold block">External Search Shortcuts:</span>
                    <a
                      href={`https://leetcode.com/problemset/?search=${selectedNum}`}
                      target="_blank"
                      rel="noreferrer"
                      className="w-full flex items-center justify-between p-2.5 bg-white border-[3px] border-ink-black text-xs font-bold text-ink-black hover:bg-slate-100 shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer"
                    >
                      <span className="flex items-center gap-2">
                        <ExternalLink className="w-4 h-4 text-emerald-500" />
                        Search LeetCode
                      </span>
                      <ArrowLeft className="w-3.5 h-3.5 rotate-180 text-trash-gray" />
                    </a>
                    <a
                      href={`https://www.google.com/search?q=leetcode+${selectedNum}`}
                      target="_blank"
                      rel="noreferrer"
                      className="w-full flex items-center justify-between p-2.5 bg-white border-[3px] border-ink-black text-xs font-bold text-ink-black hover:bg-slate-100 shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none transition-all cursor-pointer"
                    >
                      <span className="flex items-center gap-2">
                        <Search className="w-4 h-4 text-primary" />
                        Search Google
                      </span>
                      <ArrowLeft className="w-3.5 h-3.5 rotate-180 text-trash-gray" />
                    </a>
                  </div>
                </div>
              )}

              {/* Completion Toggle */}
              <div className="mt-4 pt-3 border-t-2 border-dashed border-ink-black flex flex-col gap-2">
                <span className="text-[8px] font-arcade text-trash-gray font-bold uppercase block">Quick Action</span>
                
                <button
                  onClick={() => handleToggleCompletion(selectedNum)}
                  disabled={toggleLoading}
                  className={`w-full py-3 border-[3px] border-ink-black font-arcade text-[10px] font-bold tracking-wide uppercase flex items-center justify-center gap-2 cursor-pointer transition-all shadow-[3px_3px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none disabled:opacity-50 ${
                    completions.has(selectedNum)
                      ? 'bg-red-100 text-red-600 hover:bg-red-200'
                      : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
                  }`}
                >
                  {completions.has(selectedNum) ? (
                    <>
                      <XCircle className="w-4.5 h-4.5" />
                      UNMARK AS COMPLETED
                    </>
                  ) : (
                    <>
                      <CheckCircle className="w-4.5 h-4.5" />
                      MARK AS COMPLETED
                    </>
                  )}
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}