File size: 13,892 Bytes
f91a684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Search, Cpu, Zap, BookOpen, CheckCircle2, Circle, Trophy, Loader2, Layers } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { cn } from '@/lib/utils';
import { fetchCTopics, type CChapterTopic } from '@/lib/cProgrammingClient';
import CContentPage from './CContentPage';

const STORAGE_KEY = 'ryp_c_completed';

function loadCompleted(): Record<string, boolean> {
  try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; }
}

const LEVEL_COLORS: Record<string, string> = {
  Core: 'border-blue-500/20 bg-blue-500/20 text-blue-400',
  Beginner: 'border-emerald-500/20 bg-emerald-500/20 text-emerald-400',
  Intermediate: 'border-cyan-500/20 bg-cyan-500/20 text-cyan-400',
  Advanced: 'border-purple-500/20 bg-purple-500/20 text-purple-400',
};

interface CLearningPathProps {
  onBack: () => void;
}

export default function CLearningPath({ onBack }: CLearningPathProps) {
  const [topics, setTopics] = useState<CChapterTopic[]>([]);
  const [loading, setLoading] = useState(true);
  const [selectedTopic, setSelectedTopic] = useState<CChapterTopic | null>(null);
  const [searchQuery, setSearchQuery] = useState('');
  const [completed, setCompleted] = useState<Record<string, boolean>>(loadCompleted);

  useEffect(() => {
    fetchCTopics()
      .then(data => {
        setTopics(data);
        setLoading(false);
      })
      .catch(err => {
        console.error('Failed to fetch C topics:', err);
        setLoading(false);
      });
  }, []);

  const toggleTopic = useCallback((id: string) => {
    setCompleted(prev => {
      const next = { ...prev, [id]: !prev[id] };
      localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
      return next;
    });
  }, []);

  const totalDone = topics.filter(t => completed[t.id]).length;
  const totalAll = topics.length;
  const totalSections = useMemo(() => topics.reduce((sum, topic) => sum + topic.section_count, 0), [topics]);
  const totalContent = useMemo(() => topics.reduce((sum, topic) => sum + topic.content_count, 0), [topics]);
  const pct = totalAll > 0 ? Math.round((totalDone / totalAll) * 100) : 0;

  const filteredTopics = topics.filter(topic => {
    const query = searchQuery.toLowerCase();
    return (
      topic.chapter_name.toLowerCase().includes(query) ||
      topic.subtitle?.toLowerCase().includes(query) ||
      topic.sections.some(section =>
        section.section.toLowerCase().includes(query) ||
        section.section_title.toLowerCase().includes(query)
      )
    );
  });

  return (
    <div className="mx-auto max-w-5xl space-y-8 pb-12">
      <AnimatePresence mode="wait">
        {selectedTopic ? (
          <CContentPage
            key="content"
            chapterNo={selectedTopic.chapter_no}
            onBack={() => setSelectedTopic(null)}
          />
        ) : (
          <motion.div
            key="list"
            initial={{ opacity: 0, y: 18 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -18 }}
            className="space-y-8"
          >
            <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
              <div>
                <div className="flex items-center gap-2 text-xs font-black uppercase tracking-[0.28em] text-blue-300/80">
                  <Cpu size={16} /> Coding Language
                </div>
                <h2 className="mt-3 text-4xl font-black tracking-tight text-white">C Programming</h2>
                <p className="mt-2 max-w-2xl text-sm leading-6 text-slate-400">
                  Study C from the fundamentals of syntax and memory to files, preprocessors, linked lists, and common pitfalls.
                </p>
                <button
                  onClick={onBack}
                  className="mt-4 text-xs font-bold text-blue-400 underline underline-offset-4 hover:text-blue-300"
                >
                  Back to Coding Languages
                </button>
              </div>
              <div className="relative mt-1 shrink-0">
                <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500" size={16} />
                <input
                  type="text"
                  placeholder="Search chapters or topics..."
                  value={searchQuery}
                  onChange={e => setSearchQuery(e.target.value)}
                  className="h-11 w-72 rounded-2xl border border-zinc-800 bg-zinc-950/60 pl-10 pr-4 text-sm text-white outline-none transition-all placeholder-zinc-600 focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/30"
                />
              </div>
            </div>

            <div className="relative overflow-hidden rounded-[2rem] border border-blue-500/20 bg-[#07111f]/95 p-8 shadow-[0_24px_80px_-60px_rgba(59,130,246,0.5)] backdrop-blur-xl">
              <div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(59,130,246,0.16),transparent_55%)]" />
              <div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,rgba(14,165,233,0.08),transparent_50%)]" />
              <div className="relative z-10 flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
                <div className="flex items-center gap-5">
                  <div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl border border-blue-500/20 bg-blue-500/10 text-blue-400 shadow-inner">
                    <BookOpen size={28} />
                  </div>
                  <div>
                    <div className="inline-flex items-center gap-2 rounded-full border border-blue-400/20 bg-blue-400/10 px-3 py-1">
                      <Zap size={12} className="text-blue-300" />
                      <span className="text-[10px] font-black uppercase tracking-widest text-blue-300">Track Progress</span>
                    </div>
                    <div className="mt-2 flex items-end gap-2">
                      <span className="text-4xl font-black text-white">{totalDone}</span>
                      <span className="mb-1 text-lg font-bold text-zinc-500">/ {totalAll} chapters</span>
                    </div>
                    <div className="mt-2 flex flex-wrap gap-2 text-xs font-semibold text-zinc-500">
                      <span>{totalSections} database topics</span>
                      <span className="text-zinc-700">|</span>
                      <span>{totalContent} with stored details</span>
                    </div>
                  </div>
                </div>
                <div className="mt-6 flex w-full flex-col items-start gap-3 md:mt-0 md:w-auto md:min-w-[200px] md:items-end">
                  <div className="flex w-full justify-between text-xs font-bold text-zinc-400 md:w-auto">
                    <span>Completion</span>
                    <span className={pct === 100 ? 'text-blue-400' : 'text-cyan-400'}>{pct}%</span>
                  </div>
                  <div className="h-3 w-full overflow-hidden rounded-full bg-zinc-800/80">
                    <motion.div
                      className={cn('h-full rounded-full', pct === 100
                        ? 'bg-gradient-to-r from-blue-500 to-blue-400'
                        : 'bg-gradient-to-r from-blue-500 to-cyan-500'
                      )}
                      initial={{ width: 0 }}
                      animate={{ width: `${pct}%` }}
                      transition={{ duration: 0.8, ease: 'easeOut' }}
                    />
                  </div>
                  {pct === 100 && (
                    <div className="flex w-fit items-center gap-1.5 rounded-full border border-blue-500/30 bg-blue-500/10 px-4 py-1.5 text-xs font-black text-blue-400">
                      <Trophy size={12} /> C Mastered!
                    </div>
                  )}
                </div>
              </div>
            </div> 

            {loading ? (
              <div className="flex h-40 items-center justify-center">
                <Loader2 className="h-8 w-8 animate-spin text-blue-500" />
              </div>
            ) : (
              <section>
                <div className="mb-4 flex items-center justify-between">
                  <div className="flex items-center gap-3">
                    <div className="h-5 w-1.5 rounded-full bg-gradient-to-b from-blue-400 to-cyan-600" />
                    <h3 className="text-lg font-black tracking-tight text-white">Course Chapters</h3>
                    <span className="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-0.5 text-[10px] font-black uppercase tracking-widest text-zinc-400">
                      {filteredTopics.length} chapters
                    </span>
                  </div>
                </div>

                <div className="overflow-hidden rounded-[20px] border border-zinc-800/80 bg-zinc-950/50 shadow-xl backdrop-blur-sm">
                  <div className="border-b border-zinc-800/60 bg-zinc-900/60 px-5 py-2.5 text-[9px] font-black uppercase tracking-[0.28em] text-zinc-600">
                    <div className="grid grid-cols-[28px_28px_1fr_auto] items-center gap-4">
                      <span>Done</span><span>#</span><span>Chapter</span><span className="text-right">Action</span>
                    </div>
                  </div>
                  {filteredTopics.map((topic, idx) => {
                    const done = !!completed[topic.id];
                    return (
                      <motion.div
                        key={topic.id}
                        initial={{ opacity: 0, y: 6 }}
                        animate={{ opacity: 1, y: 0 }}
                        transition={{ delay: idx * 0.025 }}
                        className={cn(
                          'group flex items-center gap-4 border-b border-zinc-800/40 px-5 py-4 transition-colors last:border-b-0',
                          idx % 2 === 0 ? 'bg-zinc-950/30' : 'bg-zinc-900/20',
                          done ? 'bg-blue-500/[0.03]' : 'cursor-pointer hover:bg-zinc-800/50',
                        )}
                        onClick={(e) => {
                          if ((e.target as HTMLElement).closest('button.checkbox-btn')) return;
                          setSelectedTopic(topic);
                        }}
                      >
                        <button type="button" onClick={() => toggleTopic(topic.id)} className="checkbox-btn shrink-0 transition-transform hover:scale-110">
                          {done
                            ? <CheckCircle2 size={18} className="text-blue-400" />
                            : <Circle size={18} className="text-zinc-600 group-hover:text-zinc-400" />}
                        </button>
                        <div className={cn('flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-black',
                          done ? 'bg-blue-500/20 text-blue-400' : 'bg-zinc-800 text-zinc-500')}>
                          {topic.chapter_no}
                        </div>
                        <div className="min-w-0 flex-1">
                          <div className={cn('flex items-center gap-2 text-sm font-semibold transition-colors',
                            done ? 'text-blue-400 line-through decoration-blue-500/40' : 'text-zinc-200 group-hover:text-white')}>
                            <span className="truncate">{topic.chapter_name}</span>
                            <span className={cn('rounded-full border px-2 py-0.5 text-[10px] font-medium no-underline', LEVEL_COLORS[topic.level] ?? LEVEL_COLORS.Core)}>
                              {topic.level}
                            </span>
                          </div>
                          <div className="mt-1 flex flex-wrap items-center gap-2">
                            <span className="inline-flex items-center gap-1 text-xs text-zinc-500">
                              <Layers size={12} />
                              {topic.section_count} topics
                            </span>
                            {topic.sections.slice(0, 3).map(section => (
                              <span key={section.id || section.section} className="max-w-[160px] truncate rounded-lg border border-zinc-800 bg-zinc-900/70 px-2 py-0.5 text-[10px] font-medium text-zinc-500">
                                {section.section} {section.section_title}
                              </span>
                            ))}
                            {topic.sections.length > 3 && (
                              <span className="text-[10px] font-bold text-zinc-600">+{topic.sections.length - 3} more</span>
                            )}
                          </div>
                        </div>
                        <button
                          className="shrink-0 flex items-center gap-1.5 rounded-xl border border-blue-500/20 bg-blue-500/10 px-3 py-1.5 text-xs font-bold text-blue-400 shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all hover:bg-blue-500/20 hover:text-blue-300 group-hover:shadow-[0_0_20px_rgba(59,130,246,0.3)]"
                          onClick={(e) => {
                            e.stopPropagation();
                            setSelectedTopic(topic);
                          }}
                        >
                          <BookOpen size={12} /> Study
                        </button>
                      </motion.div>
                    );
                  })}
                  {filteredTopics.length === 0 && !loading && (
                    <div className="p-8 text-center text-sm text-zinc-500">
                      No chapters found.
                    </div>
                  )}
                </div>
              </section>
            )}
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}