1
- import React , { ReactNode , useState , useEffect } from "react" ;
1
+ import React , { ReactNode , useState , useEffect , useRef } from "react" ;
2
2
import Layout from "@theme/Layout" ;
3
3
import Link from "@docusaurus/Link" ;
4
4
import useDocusaurusContext from "@docusaurus/useDocusaurusContext" ;
5
5
import { motion , useAnimation , useInView } from "framer-motion" ;
6
+ import Head from '@docusaurus/Head' ;
6
7
import styles from "./styles.module.css" ;
7
- import Head from "@docusaurus/Head" ;
8
8
9
9
// Type definitions
10
10
interface AnimatedSectionProps {
@@ -30,15 +30,18 @@ interface LearningPathProps {
30
30
description : string ;
31
31
icon : string ;
32
32
color : string ;
33
+ isCompleted ?: boolean ;
34
+ onToggleComplete ?: ( ) => void ;
35
+ index ?: number ;
33
36
}
34
37
35
38
// Animated component for staggered animations
36
39
const AnimatedSection = ( { children, delay = 0 } : AnimatedSectionProps ) => {
37
40
const controls = useAnimation ( ) ;
38
- const ref = React . useRef < HTMLDivElement > ( null ) ;
41
+ const ref = useRef < HTMLDivElement > ( null ) ;
39
42
const isInView = useInView ( ref , { once : true } ) ;
40
43
41
- React . useEffect ( ( ) => {
44
+ useEffect ( ( ) => {
42
45
if ( isInView ) {
43
46
controls . start ( {
44
47
opacity : 1 ,
@@ -256,10 +259,10 @@ function Feature({ title, description, icon, color }: FeatureProps) {
256
259
const [ isHovered , setIsHovered ] = useState ( false ) ;
257
260
258
261
// Create a ref to apply the custom property
259
- const featureRef = React . useRef < HTMLElement > ( null ) ;
262
+ const featureRef = useRef < HTMLElement > ( null ) ;
260
263
261
264
// Apply the color as a CSS variable when the component mounts or color changes
262
- React . useEffect ( ( ) => {
265
+ useEffect ( ( ) => {
263
266
if ( featureRef . current ) {
264
267
featureRef . current . style . setProperty ( '--feature-color' , color ) ;
265
268
}
@@ -354,7 +357,15 @@ function StatCard({ value, label, index = 0 }: StatCardProps) {
354
357
) ;
355
358
}
356
359
357
- const LearningPath = ( { title, description, icon, color, index } : LearningPathProps & { index : number } ) => {
360
+ const LearningPath = ( {
361
+ title,
362
+ description,
363
+ icon,
364
+ color,
365
+ index = 0 ,
366
+ isCompleted = false ,
367
+ onToggleComplete = ( ) => { }
368
+ } : LearningPathProps ) => {
358
369
const isEven = index % 2 === 0 ;
359
370
const delay = index * 0.15 ;
360
371
@@ -363,7 +374,7 @@ const LearningPath = ({ title, description, icon, color, index }: LearningPathPr
363
374
364
375
return (
365
376
< motion . article
366
- className = { `${ styles . pathCard } ${ isEven ? styles . left : styles . right } group` }
377
+ className = { `${ styles . pathCard } ${ isEven ? styles . left : styles . right } group relative ` }
367
378
style = { {
368
379
'--card-color' : color || '#3b82f6' ,
369
380
'--card-color-light' : color ? `${ color } 20` : 'rgba(59, 130, 246, 0.2)' ,
@@ -373,10 +384,14 @@ const LearningPath = ({ title, description, icon, color, index }: LearningPathPr
373
384
transform : isEven ? 'none' : 'translateX(50%)' ,
374
385
marginLeft : isEven ? '0' : 'auto' ,
375
386
marginRight : isEven ? 'auto' : '0' ,
387
+ opacity : isCompleted ? 0.9 : 1 ,
388
+ filter : isCompleted ? 'saturate(0.9)' : 'none' ,
389
+ transition : 'all 0.3s ease-in-out' ,
390
+ border : isCompleted ? `1px solid ${ color } 40` : '1px solid rgba(255, 255, 255, 0.1)'
376
391
} as React . CSSProperties }
377
392
initial = { { opacity : 0 , x : isEven ? - 100 : 100 , y : 20 } }
378
393
animate = { {
379
- opacity : 1 ,
394
+ opacity : 1 ,
380
395
x : 0 ,
381
396
y : 0 ,
382
397
transition : {
@@ -389,14 +404,15 @@ const LearningPath = ({ title, description, icon, color, index }: LearningPathPr
389
404
>
390
405
< div className = "flex items-start gap-4" >
391
406
< motion . div
392
- className = "flex-shrink-0 w-14 h-14 flex items-center justify-center rounded-xl text-2xl"
407
+ className = "flex-shrink-0 w-14 h-14 flex items-center justify-center rounded-xl text-2xl relative "
393
408
style = { {
394
409
background : `linear-gradient(135deg, ${ color || '#3b82f6' } , ${ color ? `${ color } 80` : '#2563eb' } )` ,
395
- boxShadow : `0 4px 15px -5px ${ color || '#3b82f6' } 80`
410
+ boxShadow : `0 4px 15px -5px ${ color || '#3b82f6' } 80` ,
411
+ opacity : isCompleted ? 0.9 : 1
396
412
} }
397
413
whileHover = { {
398
414
scale : 1.1 ,
399
- rotate : 5 ,
415
+ rotate : isCompleted ? 0 : 5 ,
400
416
transition : { duration : 0.2 }
401
417
} }
402
418
>
@@ -405,33 +421,131 @@ const LearningPath = ({ title, description, icon, color, index }: LearningPathPr
405
421
< div className = "flex-1" >
406
422
< h3 className = "text-xl font-bold mb-2 text-white" > { title } </ h3 >
407
423
< p className = "text-gray-300 mb-4 leading-relaxed" > { description } </ p >
408
- < Link
409
- to = "/docs"
410
- className = "inline-flex items-center text-sm font-medium text-blue-400 hover:text-blue-300 transition-colors group-hover:underline"
411
- >
412
- Start Learning
413
- < svg
414
- className = "w-4 h-4 ml-2 transition-transform duration-300 group-hover:translate-x-1"
415
- fill = "none"
416
- viewBox = "0 0 24 24"
417
- stroke = "currentColor"
424
+
425
+ < div className = "flex items-center justify-between mt-4 pt-3 border-t border-gray-700/50" >
426
+ < Link
427
+ to = "/docs"
428
+ className = "inline-flex items-center text-sm font-medium text-blue-400 hover:text-blue-300 transition-colors group-hover:underline"
418
429
>
419
- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M9 5l7 7-7 7" />
420
- </ svg >
421
- </ Link >
430
+ { isCompleted ? 'Continue Learning' : 'Start Learning' }
431
+ < svg
432
+ className = "w-4 h-4 ml-2 transition-transform duration-300 group-hover:translate-x-1"
433
+ fill = "none"
434
+ viewBox = "0 0 24 24"
435
+ stroke = "currentColor"
436
+ >
437
+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M9 5l7 7-7 7" />
438
+ </ svg >
439
+ </ Link >
440
+
441
+ { /* Completion Toggle */ }
442
+ < button
443
+ onClick = { ( e ) => {
444
+ e . preventDefault ( ) ;
445
+ onToggleComplete ( ) ;
446
+ } }
447
+ className = { `relative inline-flex items-center h-6 rounded-full w-11 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 ${
448
+ isCompleted
449
+ ? 'bg-green-500 focus:ring-green-500'
450
+ : 'bg-gray-600 focus:ring-blue-500'
451
+ } `}
452
+ aria-pressed = { isCompleted }
453
+ aria-label = { isCompleted ? 'Mark as incomplete' : 'Mark as complete' }
454
+ >
455
+ < span className = "sr-only" >
456
+ { isCompleted ? 'Mark as incomplete' : 'Mark as complete' }
457
+ </ span >
458
+ < span
459
+ className = { `${
460
+ isCompleted ? 'translate-x-6' : 'translate-x-1'
461
+ } inline-block w-4 h-4 transform bg-white rounded-full transition-transform duration-200 ease-in-out`}
462
+ >
463
+ { isCompleted && (
464
+ < svg
465
+ className = "w-3 h-3 text-green-500 mx-auto mt-0.5"
466
+ fill = "currentColor"
467
+ viewBox = "0 0 20 20"
468
+ xmlns = "http://www.w3.org/2000/svg"
469
+ >
470
+ < path
471
+ fillRule = "evenodd"
472
+ d = "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
473
+ clipRule = "evenodd"
474
+ />
475
+ </ svg >
476
+ ) }
477
+ </ span >
478
+ </ button >
479
+ </ div >
422
480
</ div >
423
481
</ div >
482
+
483
+ { /* Progress bar background */ }
484
+ < div className = "absolute bottom-0 left-0 right-0 h-1 bg-gray-700/50 rounded-b-lg overflow-hidden" >
485
+ < motion . div
486
+ className = "h-full bg-green-500"
487
+ initial = { { width : '0%' } }
488
+ animate = { { width : isCompleted ? '100%' : '0%' } }
489
+ transition = { { duration : 0.5 , ease : 'easeOut' } }
490
+ />
491
+ </ div >
492
+
493
+ { /* Completion badge */ }
494
+ { isCompleted && (
495
+ < div className = "absolute -top-2 -right-2 w-8 h-8 bg-green-500 rounded-full flex items-center justify-center shadow-lg" >
496
+ < svg className = "w-4 h-4 text-white" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
497
+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 3 } d = "M5 13l4 4L19 7" />
498
+ </ svg >
499
+ </ div >
500
+ ) }
424
501
</ motion . article >
425
502
) ;
426
503
} ;
427
504
428
505
export default function GetStarted ( ) {
429
506
const { siteConfig } = useDocusaurusContext ( ) ;
430
- const currentYear = new Date ( ) . getFullYear ( ) ;
507
+ type CompletedPaths = Record < string , boolean > ;
508
+
509
+ const [ completedPaths , setCompletedPaths ] = useState < CompletedPaths > ( ( ) => {
510
+ if ( typeof window !== 'undefined' ) {
511
+ try {
512
+ const saved = localStorage . getItem ( 'completedPaths' ) ;
513
+ return saved ? JSON . parse ( saved ) : { } ;
514
+ } catch ( e ) {
515
+ console . error ( 'Failed to parse completedPaths from localStorage' , e ) ;
516
+ return { } ;
517
+ }
518
+ }
519
+ return { } ;
520
+ } ) ;
521
+
522
+ // Toggle completion status for a path
523
+ const togglePathCompletion = React . useCallback ( ( pathTitle : string ) => {
524
+ setCompletedPaths ( prev => ( {
525
+ ...prev ,
526
+ [ pathTitle ] : ! prev [ pathTitle ]
527
+ } ) ) ;
528
+ } , [ ] ) ;
529
+
530
+ // Calculate completion percentage
531
+ const completionPercentage = Math . round (
532
+ ( Object . values ( completedPaths ) . filter ( Boolean ) . length / learningPaths . length ) * 100
533
+ ) || 0 ;
534
+
535
+ // Save to localStorage whenever completedPaths changes
536
+ useEffect ( ( ) => {
537
+ if ( typeof window !== 'undefined' ) {
538
+ try {
539
+ localStorage . setItem ( 'completedPaths' , JSON . stringify ( completedPaths ) ) ;
540
+ } catch ( e ) {
541
+ console . error ( 'Failed to save completedPaths to localStorage' , e ) ;
542
+ }
543
+ }
544
+ } , [ completedPaths ] ) ;
431
545
432
546
return (
433
547
< Layout
434
- title = { `Get Started | ${ siteConfig . title } ` }
548
+ title = { `Get Started | ${ siteConfig ? .title || 'Recode Hive' } ` }
435
549
description = "Start your coding journey with Recode Hive. Learn to code with our interactive platform and structured learning paths."
436
550
>
437
551
< Head >
@@ -517,19 +631,83 @@ export default function GetStarted() {
517
631
</ motion . h2 >
518
632
</ div >
519
633
520
- < div className = { styles . timelineContainer } >
521
- < div className = { styles . timeline } > </ div >
522
- < div className = { styles . pathsContainer } >
523
- { learningPaths . map ( ( path , idx ) => (
524
- < LearningPath
525
- key = { path . title }
526
- title = { path . title }
527
- description = { path . description }
528
- icon = { path . icon }
529
- color = { path . color }
530
- index = { idx }
634
+ < div className = "relative" >
635
+ { /* Progress Overview */ }
636
+ < motion . div
637
+ className = "mb-12 p-6 bg-gray-800/50 backdrop-blur-sm rounded-2xl border border-gray-700/50 shadow-lg"
638
+ initial = { { opacity : 0 , y : 20 } }
639
+ whileInView = { { opacity : 1 , y : 0 } }
640
+ viewport = { { once : true , margin : "-50px 0px -50px 0px" } }
641
+ transition = { { duration : 0.5 , delay : 0.2 } }
642
+ >
643
+ < div className = "flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4" >
644
+ < div >
645
+ < h3 className = "text-xl font-bold text-white mb-1" > Your Learning Progress</ h3 >
646
+ < p className = "text-gray-300 text-sm" >
647
+ { completionPercentage } % Complete • { Object . values ( completedPaths ) . filter ( Boolean ) . length } of { learningPaths . length } paths started
648
+ </ p >
649
+ </ div >
650
+ < div className = "text-right" >
651
+ < span className = "text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent" >
652
+ { completionPercentage } %
653
+ </ span >
654
+ </ div >
655
+ </ div >
656
+
657
+ { /* Progress Bar */ }
658
+ < div className = "w-full h-3 bg-gray-700/50 rounded-full overflow-hidden" >
659
+ < motion . div
660
+ className = "h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full relative"
661
+ initial = { { width : 0 } }
662
+ animate = { { width : `${ completionPercentage } %` } }
663
+ transition = { { duration : 1 , ease : "easeOut" } }
664
+ >
665
+ < motion . div
666
+ className = "absolute inset-0 bg-white/20"
667
+ animate = { {
668
+ left : [ '0%' , '100%' ]
669
+ } }
670
+ transition = { {
671
+ duration : 1.5 ,
672
+ repeat : Infinity ,
673
+ repeatType : 'loop' ,
674
+ ease : 'easeInOut' ,
675
+ } }
676
+ style = { {
677
+ width : '20%' ,
678
+ height : '100%' ,
679
+ background : 'linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent)' ,
680
+ transform : 'skewX(-20deg)'
681
+ } }
682
+ />
683
+ </ motion . div >
684
+ </ div >
685
+ </ motion . div >
686
+
687
+ < div className = { styles . timelineContainer } >
688
+ < div className = { styles . timeline } >
689
+ { /* Animated progress indicator on the timeline */ }
690
+ < motion . div
691
+ className = "absolute left-0 top-0 w-1 bg-gradient-to-b from-blue-400 to-purple-500 h-0"
692
+ initial = { { height : 0 } }
693
+ animate = { { height : `${ ( completionPercentage / 100 ) * 100 } %` } }
694
+ transition = { { duration : 1.5 , ease : 'easeInOut' } }
531
695
/>
532
- ) ) }
696
+ </ div >
697
+ < div className = { styles . pathsContainer } >
698
+ { learningPaths . map ( ( path , idx ) => (
699
+ < LearningPath
700
+ key = { path . title }
701
+ title = { path . title }
702
+ description = { path . description }
703
+ icon = { path . icon }
704
+ color = { path . color }
705
+ index = { idx }
706
+ isCompleted = { ! ! completedPaths [ path . title ] }
707
+ onToggleComplete = { ( ) => togglePathCompletion ( path . title ) }
708
+ />
709
+ ) ) }
710
+ </ div >
533
711
</ div >
534
712
</ div >
535
713
0 commit comments