From 135ce63c1a4ac1bbeaa9a403e00b30cf07732c66 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Wed, 10 Sep 2025 12:56:31 -0300 Subject: [PATCH 01/19] wip first step --- docs/tutorials/near-quest.md | 137 +++++ docs/tutorials/welcome.md | 7 + website/package.json | 4 +- website/sidebars.js | 1 + .../AchievementToast/AchievementToast.jsx | 97 +++ .../AchievementToast.module.scss | 410 +++++++++++++ .../AchievementToast/AchievementToast.scss | 361 +++++++++++ .../src/components/AchievementToast/index.js | 2 + .../AchievementToast/useAchievements.js | 265 +++++++++ .../InteractiveLessonComponent.jsx | 226 +++++++ .../InteractiveLessonComponent.scss | 211 +++++++ .../LessonOutline.jsx | 26 + .../LessonOutline.scss | 130 ++++ .../LessonProgress.jsx | 28 + .../LessonProgress.scss | 80 +++ .../QuestionCard.jsx | 78 +++ .../QuestionCard.scss | 233 ++++++++ .../InteractiveLessonComponent/index.js | 11 + .../src/components/MultiStep/MultiStep.jsx | 249 ++++++++ .../src/components/MultiStep/MultiStep.scss | 268 +++++++++ .../components/MultiStep/MultiStepExample.jsx | 211 +++++++ .../MultiStep/MultiStepExample.scss | 263 ++++++++ .../components/MultiStep/NavigationBar.jsx | 141 +++++ .../components/MultiStep/NavigationBar.scss | 218 +++++++ .../src/components/MultiStep/ProgressBar.jsx | 91 +++ .../src/components/MultiStep/ProgressBar.scss | 307 ++++++++++ website/src/components/MultiStep/index.js | 11 + .../QuestAchievementDemo.jsx | 241 ++++++++ .../QuestAchievementDemo.module.scss | 563 ++++++++++++++++++ .../QuestAchievementDemo.scss | 373 ++++++++++++ .../components/QuestAchievementDemo/index.js | 1 + website/src/components/Quiz/index.js | 199 +++++++ .../DecentralizedOrganization/constants.js | 0 .../hooks/useFormValidation.js | 0 .../hooks/useImageUpload.js | 0 .../steps/BasicInfoStep.jsx | 0 .../steps/CouncilsStep.jsx | 0 .../steps/DesignStep.jsx | 0 .../steps/ReviewStep.jsx | 0 .../utils/validation.js | 0 website/yarn.lock | 10 +- 41 files changed, 5446 insertions(+), 7 deletions(-) create mode 100644 docs/tutorials/near-quest.md create mode 100644 website/src/components/AchievementToast/AchievementToast.jsx create mode 100644 website/src/components/AchievementToast/AchievementToast.module.scss create mode 100644 website/src/components/AchievementToast/AchievementToast.scss create mode 100644 website/src/components/AchievementToast/index.js create mode 100644 website/src/components/AchievementToast/useAchievements.js create mode 100644 website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.jsx create mode 100644 website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.scss create mode 100644 website/src/components/InteractiveLessonComponent/LessonOutline.jsx create mode 100644 website/src/components/InteractiveLessonComponent/LessonOutline.scss create mode 100644 website/src/components/InteractiveLessonComponent/LessonProgress.jsx create mode 100644 website/src/components/InteractiveLessonComponent/LessonProgress.scss create mode 100644 website/src/components/InteractiveLessonComponent/QuestionCard.jsx create mode 100644 website/src/components/InteractiveLessonComponent/QuestionCard.scss create mode 100644 website/src/components/InteractiveLessonComponent/index.js create mode 100644 website/src/components/MultiStep/MultiStep.jsx create mode 100644 website/src/components/MultiStep/MultiStep.scss create mode 100644 website/src/components/MultiStep/MultiStepExample.jsx create mode 100644 website/src/components/MultiStep/MultiStepExample.scss create mode 100644 website/src/components/MultiStep/NavigationBar.jsx create mode 100644 website/src/components/MultiStep/NavigationBar.scss create mode 100644 website/src/components/MultiStep/ProgressBar.jsx create mode 100644 website/src/components/MultiStep/ProgressBar.scss create mode 100644 website/src/components/MultiStep/index.js create mode 100644 website/src/components/QuestAchievementDemo/QuestAchievementDemo.jsx create mode 100644 website/src/components/QuestAchievementDemo/QuestAchievementDemo.module.scss create mode 100644 website/src/components/QuestAchievementDemo/QuestAchievementDemo.scss create mode 100644 website/src/components/QuestAchievementDemo/index.js create mode 100644 website/src/components/Quiz/index.js create mode 100644 website/src/components/tools/DecentralizedOrganization/constants.js create mode 100644 website/src/components/tools/DecentralizedOrganization/hooks/useFormValidation.js create mode 100644 website/src/components/tools/DecentralizedOrganization/hooks/useImageUpload.js create mode 100644 website/src/components/tools/DecentralizedOrganization/steps/BasicInfoStep.jsx create mode 100644 website/src/components/tools/DecentralizedOrganization/steps/CouncilsStep.jsx create mode 100644 website/src/components/tools/DecentralizedOrganization/steps/DesignStep.jsx create mode 100644 website/src/components/tools/DecentralizedOrganization/steps/ReviewStep.jsx create mode 100644 website/src/components/tools/DecentralizedOrganization/utils/validation.js diff --git a/docs/tutorials/near-quest.md b/docs/tutorials/near-quest.md new file mode 100644 index 00000000000..8d8ad971289 --- /dev/null +++ b/docs/tutorials/near-quest.md @@ -0,0 +1,137 @@ +--- +id: near-quest +title: NEAR Quest +sidebar_label: NEAR Quest +description: Learn NEAR development through interactive quests and challenges designed to build your skills step by step. +hide_table_of_contents: true +--- + +import QuestAchievementDemo from "@site/src/components/QuestAchievementDemo"; +import { MultiStepExample } from "@site/src/components/MultiStep"; +import { InteractiveLessonComponent } from "@site/src/components/InteractiveLessonComponent"; + +Welcome to NEAR Quest - an interactive learning journey designed to help you master NEAR Protocol development through hands-on challenges and guided quests. + +## What is NEAR Quest? + +NEAR Quest is a gamified learning platform that takes you through progressively challenging tasks to build your skills in: + +- Smart contract development +- Frontend integration +- NEAR Protocol fundamentals +- Advanced blockchain concepts +- Real-world project building + +## Getting Started + +### Prerequisites + +Before starting your quest, make sure you have: + +- Basic knowledge of JavaScript or Rust +- Node.js and npm installed +- A NEAR testnet account +- Code editor (VS Code recommended) + +### Your First Quest + +Ready to begin your journey? Start with these foundational quests: + +1. **Account Creation Quest** - Learn to create and manage NEAR accounts +2. **Smart Contract Basics** - Deploy your first contract +3. **Frontend Integration** - Connect your contract to a web interface +4. **Token Adventures** - Explore FT and NFT development + +## Quest Categories + +### Beginner Quests + +Perfect for developers new to NEAR: + +- Setting up your development environment +- Understanding NEAR accounts and keys +- Basic smart contract patterns +- Simple frontend integration + +### Intermediate Quests + +Build on the fundamentals: + +- Cross-contract calls +- State management +- Token standards implementation +- Testing strategies + +### Advanced Quests + +Master complex concepts: + +- Chain signatures and multi-chain development +- Advanced security patterns +- Performance optimization +- Production deployment strategies + +### Challenge Quests + +Test your skills with real-world scenarios: + +- DeFi protocol implementation +- NFT marketplace creation +- DAO governance systems +- Gaming applications + +## How Quests Work + +Each quest includes: + +- **Objective**: Clear goals and success criteria +- **Learning Resources**: Documentation and tutorials +- **Code Challenges**: Hands-on coding exercises +- **Validation**: Automated testing and verification +- **Rewards**: XP points and achievement badges + +:::tip Try the Interactive Learning System +Experience NEAR Quest's interactive lesson format below! This demo shows how lessons are structured with content, progress tracking, and review questions to reinforce your learning. +::: + + + +:::tip Try the Achievement System +Click the buttons below to experience NEAR Quest's gamified achievement system in action! The toast notifications are inspired by Steam's achievement design but tailored for Web3 development milestones. +::: + + + + + +## Quest Progress Tracking + +Track your learning journey: + +- Complete quests to earn XP +- Unlock achievement badges +- View your skill progression +- Share your accomplishments + +## Community and Support + +Join the NEAR Quest community: + +- Discord channels for quest discussions +- Peer code reviews and collaboration +- Regular community challenges +- Developer office hours + +## Start Your Quest + +Ready to begin? Choose your path: + +- **New to Blockchain?** Start with [Beginner Quests](#beginner-quests) +- **Have Some Experience?** Jump into [Intermediate Quests](#intermediate-quests) +- **Blockchain Veteran?** Challenge yourself with [Advanced Quests](#advanced-quests) + +Let your NEAR development journey begin! 🚀 + +--- + +*NEAR Quest is designed to make learning blockchain development engaging and practical. Each quest builds upon previous knowledge while introducing new concepts in an approachable way.* diff --git a/docs/tutorials/welcome.md b/docs/tutorials/welcome.md index b16ddd788bc..be28d38c66c 100644 --- a/docs/tutorials/welcome.md +++ b/docs/tutorials/welcome.md @@ -18,6 +18,13 @@ import {Container, Card} from "@site/src/components/cards"; + { + const [isAnimating, setIsAnimating] = useState(false); + const [shouldRender, setShouldRender] = useState(false); + + useEffect(() => { + if (isVisible) { + setShouldRender(true); + setTimeout(() => setIsAnimating(true), 50); + + const timer = setTimeout(() => { + handleClose(); + }, 6000); + + return () => clearTimeout(timer); + } + }, [isVisible]); + + const handleClose = () => { + setIsAnimating(false); + setTimeout(() => { + setShouldRender(false); + onClose(); + }, 500); + }; + + if (!shouldRender) return null; + + const { + title = "Achievement Unlocked!", + description = "You've completed a quest!", + icon = "🏆", + xp = 100, + rarity = "common" + } = achievement || {}; + + // Ensure rarity is valid + const validRarity = ['common', 'rare', 'epic', 'legendary'].includes(rarity) ? rarity : 'common'; + + const toastClasses = [ + styles.achievementToast, + isAnimating ? styles.visible : '', + styles[validRarity] + ].filter(Boolean).join(' '); + + return ( +
+
+
+
+
{icon}
+
+ + + +
+
+ +
+
+ Achievement Unlocked! +
+ +{xp} XP + {queueLength > 0 && ( + +{queueLength} more + )} +
+
+

{title}

+

{description}

+
+ + +
+ +
+
+
+
+ ); +}; + +export default AchievementToast; diff --git a/website/src/components/AchievementToast/AchievementToast.module.scss b/website/src/components/AchievementToast/AchievementToast.module.scss new file mode 100644 index 00000000000..94600635d64 --- /dev/null +++ b/website/src/components/AchievementToast/AchievementToast.module.scss @@ -0,0 +1,410 @@ +// Achievement Toast Styles - Steam-inspired design with NEAR branding +.achievementToast { + position: fixed; + top: 20px; + right: -400px; // Start off-screen + width: 380px; + background: linear-gradient(135deg, + rgba(21, 32, 43, 0.95) 0%, + rgba(33, 47, 61, 0.95) 50%, + rgba(21, 32, 43, 0.95) 100%); + border: 2px solid #3b82f6; + border-radius: 8px; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(59, 130, 246, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + color: white; + font-family: var(--bs-font-sans-serif); + z-index: 9999; + cursor: pointer; + overflow: hidden; + backdrop-filter: blur(10px); + transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); + + &:hover { + transform: translateX(-5px) scale(1.02); + box-shadow: + 0 15px 40px rgba(0, 0, 0, 0.6), + 0 0 25px rgba(59, 130, 246, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + } +} + +.visible { + right: 20px; + transform: translateX(0); +} + +// Rarity variants +.common { + border-color: #6b7280; + + .glow { + background: radial-gradient(circle at center, rgba(107, 114, 128, 0.3) 0%, transparent 70%); + } +} + +.rare { + border-color: #3b82f6; + + .glow { + background: radial-gradient(circle at center, rgba(59, 130, 246, 0.4) 0%, transparent 70%); + } +} + +.epic { + border-color: #8b5cf6; + animation: epicPulse 2s ease-in-out infinite alternate; + + .glow { + background: radial-gradient(circle at center, rgba(139, 92, 246, 0.4) 0%, transparent 70%); + } + + .iconContainer { + background: radial-gradient(circle, rgba(139, 92, 246, 0.3) 0%, rgba(139, 92, 246, 0.1) 70%); + border: 2px solid rgba(139, 92, 246, 0.6); + box-shadow: 0 0 15px rgba(139, 92, 246, 0.3); + } + + .content { + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.1) 0%, + transparent 50%, + rgba(139, 92, 246, 0.1) 100%); + } +} + +.legendary { + border-color: #f59e0b; + + .glow { + background: radial-gradient(circle at center, rgba(245, 158, 11, 0.5) 0%, transparent 70%); + } + + .content { + background: linear-gradient(135deg, + rgba(245, 158, 11, 0.1) 0%, + transparent 50%, + rgba(245, 158, 11, 0.1) 100%); + } +} + +.glow { + position: absolute; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background: radial-gradient(circle at center, rgba(59, 130, 246, 0.3) 0%, transparent 70%); + animation: pulseGlow 2s ease-in-out infinite alternate; + pointer-events: none; +} + +.content { + position: relative; + display: flex; + align-items: center; + padding: 16px; + gap: 12px; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.05) 0%, + transparent 50%, + rgba(255, 255, 255, 0.02) 100%); +} + +.iconContainer { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, rgba(59, 130, 246, 0.1) 70%); + border: 2px solid rgba(59, 130, 246, 0.5); + border-radius: 50%; + flex-shrink: 0; +} + +.icon { + font-size: 28px; + animation: bounceIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.sparkles { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +.sparkle { + position: absolute; + font-size: 12px; + animation: sparkle 2s ease-in-out infinite; +} + +.sparkle1 { + top: -5px; + right: -5px; + animation-delay: 0s; +} + +.sparkle2 { + bottom: -8px; + left: -8px; + animation-delay: 0.7s; +} + +.sparkle3 { + top: -8px; + left: 50%; + transform: translateX(-50%); + animation-delay: 1.4s; +} + +.text { + flex: 1; + min-width: 0; // Allow text to shrink +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.headerRight { + display: flex; + align-items: center; + gap: 8px; +} + +.label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: #82aaff; + letter-spacing: 0.5px; +} + +.xp { + font-size: 12px; + font-weight: 700; + color: #10b981; + background: rgba(16, 185, 129, 0.1); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.queueIndicator { + font-size: 11px; + font-weight: 600; + color: #82aaff; + background: rgba(130, 170, 255, 0.1); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid rgba(130, 170, 255, 0.3); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.title { + font-size: 16px; + font-weight: 600; + margin: 0 0 4px 0; + color: white; + line-height: 1.2; + word-wrap: break-word; +} + +.description { + font-size: 13px; + color: rgba(255, 255, 255, 0.8); + margin: 0; + line-height: 1.3; + word-wrap: break-word; +} + +.close { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + font-size: 24px; + font-weight: 300; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover { + color: white; + background: rgba(255, 255, 255, 0.1); + transform: scale(1.1); + } +} + +.progressBar { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: rgba(255, 255, 255, 0.1); + overflow: hidden; +} + +.progressFill { + width: 100%; + height: 100%; + background: linear-gradient(90deg, #3b82f6, #82aaff); + transform: translateX(-100%); + animation: progressFill 6s linear forwards; +} + +// Animations +@keyframes bounceIn { + 0% { + transform: scale(0) rotate(-180deg); + opacity: 0; + } + 50% { + transform: scale(1.2) rotate(-90deg); + opacity: 0.8; + } + 100% { + transform: scale(1) rotate(0deg); + opacity: 1; + } +} + +@keyframes sparkle { + 0%, 100% { + opacity: 0; + transform: scale(0.5) rotate(0deg); + } + 50% { + opacity: 1; + transform: scale(1.2) rotate(180deg); + } +} + +@keyframes pulseGlow { + 0% { + opacity: 0.5; + transform: scale(1); + } + 100% { + opacity: 0.8; + transform: scale(1.1); + } +} + +@keyframes progressFill { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(0%); + } +} + +@keyframes epicPulse { + 0% { + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(139, 92, 246, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + 100% { + box-shadow: + 0 15px 40px rgba(0, 0, 0, 0.6), + 0 0 30px rgba(139, 92, 246, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + } +} + +// Dark theme adjustments +:global([data-theme='dark']) .achievementToast { + background: linear-gradient(135deg, + rgba(13, 17, 23, 0.95) 0%, + rgba(33, 38, 45, 0.95) 50%, + rgba(13, 17, 23, 0.95) 100%); +} + +:global([data-theme='dark']) .legendary .content { + background: linear-gradient(135deg, + rgba(245, 158, 11, 0.15) 0%, + transparent 50%, + rgba(245, 158, 11, 0.15) 100%); +} + +// Light theme adjustments +:global([data-theme='light']) .achievementToast { + background: linear-gradient(135deg, + rgba(248, 250, 252, 0.95) 0%, + rgba(241, 245, 249, 0.95) 50%, + rgba(248, 250, 252, 0.95) 100%); + color: #1f2937; +} + +:global([data-theme='light']) .title { + color: #1f2937; +} + +:global([data-theme='light']) .description { + color: rgba(31, 41, 55, 0.8); +} + +:global([data-theme='light']) .close { + color: rgba(31, 41, 55, 0.6); + + &:hover { + color: #1f2937; + background: rgba(31, 41, 55, 0.1); + } +} + +// Responsive design +@media (max-width: 768px) { + .achievementToast { + width: calc(100vw - 40px); + right: -100vw; + } + + .visible { + right: 20px; + } + + .content { + padding: 12px; + gap: 10px; + } + + .iconContainer { + width: 50px; + height: 50px; + } + + .icon { + font-size: 24px; + } + + .title { + font-size: 14px; + } + + .description { + font-size: 12px; + } +} diff --git a/website/src/components/AchievementToast/AchievementToast.scss b/website/src/components/AchievementToast/AchievementToast.scss new file mode 100644 index 00000000000..ab3970c1b82 --- /dev/null +++ b/website/src/components/AchievementToast/AchievementToast.scss @@ -0,0 +1,361 @@ +// Achievement Toast Styles - Steam-inspired design with NEAR branding +.achievement-toast { + position: fixed; + top: 20px; + right: -400px; // Start off-screen + width: 380px; + background: linear-gradient(135deg, + rgba(21, 32, 43, 0.95) 0%, + rgba(33, 47, 61, 0.95) 50%, + rgba(21, 32, 43, 0.95) 100%); + border: 2px solid #3b82f6; + border-radius: 8px; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(59, 130, 246, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + color: white; + font-family: var(--bs-font-sans-serif); + z-index: 9999; + cursor: pointer; + overflow: hidden; + backdrop-filter: blur(10px); + transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); + + &--visible { + right: 20px; + transform: translateX(0); + } + + // Rarity variants + &--common { + border-color: #6b7280; + .achievement-toast__glow { + background: radial-gradient(circle at center, rgba(107, 114, 128, 0.3) 0%, transparent 70%); + } + } + + &--rare { + border-color: #3b82f6; + .achievement-toast__glow { + background: radial-gradient(circle at center, rgba(59, 130, 246, 0.4) 0%, transparent 70%); + } + } + + &--epic { + border-color: #8b5cf6; + .achievement-toast__glow { + background: radial-gradient(circle at center, rgba(139, 92, 246, 0.4) 0%, transparent 70%); + } + } + + &--legendary { + border-color: #f59e0b; + .achievement-toast__glow { + background: radial-gradient(circle at center, rgba(245, 158, 11, 0.5) 0%, transparent 70%); + } + + .achievement-toast__content { + background: linear-gradient(135deg, + rgba(245, 158, 11, 0.1) 0%, + transparent 50%, + rgba(245, 158, 11, 0.1) 100%); + } + } + + &:hover { + transform: translateX(-5px) scale(1.02); + box-shadow: + 0 15px 40px rgba(0, 0, 0, 0.6), + 0 0 25px rgba(59, 130, 246, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + } +} + +.achievement-toast__glow { + position: absolute; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background: radial-gradient(circle at center, rgba(59, 130, 246, 0.3) 0%, transparent 70%); + animation: pulse-glow 2s ease-in-out infinite alternate; + pointer-events: none; +} + +.achievement-toast__content { + position: relative; + display: flex; + align-items: center; + padding: 16px; + gap: 12px; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.05) 0%, + transparent 50%, + rgba(255, 255, 255, 0.02) 100%); +} + +.achievement-toast__icon-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, rgba(59, 130, 246, 0.1) 70%); + border: 2px solid rgba(59, 130, 246, 0.5); + border-radius: 50%; + flex-shrink: 0; +} + +.achievement-toast__icon { + font-size: 28px; + animation: bounce-in 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.achievement-toast__sparkles { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + + .sparkle { + position: absolute; + font-size: 12px; + animation: sparkle 2s ease-in-out infinite; + + &.sparkle-1 { + top: -5px; + right: -5px; + animation-delay: 0s; + } + + &.sparkle-2 { + bottom: -8px; + left: -8px; + animation-delay: 0.7s; + } + + &.sparkle-3 { + top: -8px; + left: 50%; + transform: translateX(-50%); + animation-delay: 1.4s; + } + } +} + +.achievement-toast__text { + flex: 1; + min-width: 0; // Allow text to shrink +} + +.achievement-toast__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.achievement-toast__label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: #82aaff; + letter-spacing: 0.5px; +} + +.achievement-toast__xp { + font-size: 12px; + font-weight: 700; + color: #10b981; + background: rgba(16, 185, 129, 0.1); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.achievement-toast__title { + font-size: 16px; + font-weight: 600; + margin: 0 0 4px 0; + color: white; + line-height: 1.2; + word-wrap: break-word; +} + +.achievement-toast__description { + font-size: 13px; + color: rgba(255, 255, 255, 0.8); + margin: 0; + line-height: 1.3; + word-wrap: break-word; +} + +.achievement-toast__close { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + font-size: 24px; + font-weight: 300; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover { + color: white; + background: rgba(255, 255, 255, 0.1); + transform: scale(1.1); + } +} + +.achievement-toast__progress-bar { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: rgba(255, 255, 255, 0.1); + overflow: hidden; +} + +.achievement-toast__progress-fill { + width: 100%; + height: 100%; + background: linear-gradient(90deg, #3b82f6, #82aaff); + transform: translateX(-100%); + animation: progress-fill 6s linear forwards; +} + +// Animations +@keyframes bounce-in { + 0% { + transform: scale(0) rotate(-180deg); + opacity: 0; + } + 50% { + transform: scale(1.2) rotate(-90deg); + opacity: 0.8; + } + 100% { + transform: scale(1) rotate(0deg); + opacity: 1; + } +} + +@keyframes sparkle { + 0%, 100% { + opacity: 0; + transform: scale(0.5) rotate(0deg); + } + 50% { + opacity: 1; + transform: scale(1.2) rotate(180deg); + } +} + +@keyframes pulse-glow { + 0% { + opacity: 0.5; + transform: scale(1); + } + 100% { + opacity: 0.8; + transform: scale(1.1); + } +} + +@keyframes progress-fill { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(0%); + } +} + +// Dark theme adjustments +[data-theme='dark'] .achievement-toast { + background: linear-gradient(135deg, + rgba(13, 17, 23, 0.95) 0%, + rgba(33, 38, 45, 0.95) 50%, + rgba(13, 17, 23, 0.95) 100%); + + &--legendary { + .achievement-toast__content { + background: linear-gradient(135deg, + rgba(245, 158, 11, 0.15) 0%, + transparent 50%, + rgba(245, 158, 11, 0.15) 100%); + } + } +} + +// Light theme adjustments +[data-theme='light'] .achievement-toast { + background: linear-gradient(135deg, + rgba(248, 250, 252, 0.95) 0%, + rgba(241, 245, 249, 0.95) 50%, + rgba(248, 250, 252, 0.95) 100%); + color: #1f2937; + + .achievement-toast__title { + color: #1f2937; + } + + .achievement-toast__description { + color: rgba(31, 41, 55, 0.8); + } + + .achievement-toast__close { + color: rgba(31, 41, 55, 0.6); + + &:hover { + color: #1f2937; + background: rgba(31, 41, 55, 0.1); + } + } +} + +// Responsive design +@media (max-width: 768px) { + .achievement-toast { + width: calc(100vw - 40px); + right: -100vw; + + &--visible { + right: 20px; + } + } + + .achievement-toast__content { + padding: 12px; + gap: 10px; + } + + .achievement-toast__icon-container { + width: 50px; + height: 50px; + } + + .achievement-toast__icon { + font-size: 24px; + } + + .achievement-toast__title { + font-size: 14px; + } + + .achievement-toast__description { + font-size: 12px; + } +} diff --git a/website/src/components/AchievementToast/index.js b/website/src/components/AchievementToast/index.js new file mode 100644 index 00000000000..45470430b08 --- /dev/null +++ b/website/src/components/AchievementToast/index.js @@ -0,0 +1,2 @@ +export { default as AchievementToast } from './AchievementToast.jsx'; +export { useAchievements, ACHIEVEMENTS } from './useAchievements.js'; diff --git a/website/src/components/AchievementToast/useAchievements.js b/website/src/components/AchievementToast/useAchievements.js new file mode 100644 index 00000000000..7c80dff289d --- /dev/null +++ b/website/src/components/AchievementToast/useAchievements.js @@ -0,0 +1,265 @@ +import React, { useState, useCallback } from 'react'; + +// Achievement definitions with different rarities and XP values +export const ACHIEVEMENTS = { + FIRST_QUEST: { + id: 'first_quest', + title: 'Getting Started', + description: 'Completed your first NEAR Quest!', + icon: '🚀', + xp: 50, + rarity: 'common' + }, + ACCOUNT_CREATED: { + id: 'account_created', + title: 'Welcome to NEAR', + description: 'Successfully created your NEAR account', + icon: '👤', + xp: 75, + rarity: 'common' + }, + FIRST_CONTRACT: { + id: 'first_contract', + title: 'Smart Contract Pioneer', + description: 'Deployed your first smart contract', + icon: '📜', + xp: 150, + rarity: 'rare' + }, + FRONTEND_MASTER: { + id: 'frontend_master', + title: 'Frontend Wizard', + description: 'Connected contract to web interface', + icon: '🎨', + xp: 200, + rarity: 'rare' + }, + TOKEN_EXPLORER: { + id: 'token_explorer', + title: 'Token Explorer', + description: 'Mastered FT and NFT development', + icon: '🪙', + xp: 250, + rarity: 'epic' + }, + DEFI_DEVELOPER: { + id: 'defi_developer', + title: 'DeFi Architect', + description: 'Built a complete DeFi protocol', + icon: '🏦', + xp: 500, + rarity: 'legendary' + }, + DAO_CREATOR: { + id: 'dao_creator', + title: 'Governance Guru', + description: 'Created a decentralized organization', + icon: '🏛️', + xp: 400, + rarity: 'epic' + }, + GAME_DEVELOPER: { + id: 'game_developer', + title: 'Blockchain Gamer', + description: 'Developed a Web3 gaming application', + icon: '🎮', + xp: 350, + rarity: 'epic' + }, + SECURITY_EXPERT: { + id: 'security_expert', + title: 'Security Guardian', + description: 'Implemented advanced security patterns', + icon: '🛡️', + xp: 300, + rarity: 'rare' + }, + PERFORMANCE_OPTIMIZER: { + id: 'performance_optimizer', + title: 'Speed Demon', + description: 'Optimized contract for maximum performance', + icon: '⚡', + xp: 275, + rarity: 'rare' + }, + CROSS_CHAIN_MASTER: { + id: 'cross_chain_master', + title: 'Chain Signatures Master', + description: 'Mastered multi-chain development', + icon: '🌐', + xp: 600, + rarity: 'legendary' + }, + QUEST_COMPLETIONIST: { + id: 'quest_completionist', + title: 'Quest Completionist', + description: 'Completed all available quests', + icon: '🏆', + xp: 1000, + rarity: 'legendary' + } +}; + +export const useAchievements = () => { + const [currentAchievement, setCurrentAchievement] = useState(null); + const [isVisible, setIsVisible] = useState(false); + const [totalXP, setTotalXP] = useState(0); + const [unlockedAchievements, setUnlockedAchievements] = useState(new Set()); + const [achievementQueue, setAchievementQueue] = useState([]); + const [isProcessingQueue, setIsProcessingQueue] = useState(false); + + // Process the queue of achievements + const processQueue = useCallback(() => { + if (isProcessingQueue || achievementQueue.length === 0 || isVisible) { + return; + } + + setIsProcessingQueue(true); + const nextAchievement = achievementQueue[0]; + setAchievementQueue(prev => prev.slice(1)); + + // Display the achievement + setCurrentAchievement(nextAchievement); + setIsVisible(true); + + // Play sound and haptic feedback + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Different sounds for different rarities + let baseFreq = 800; + switch (nextAchievement.rarity) { + case 'rare': + baseFreq = 900; + break; + case 'epic': + baseFreq = 1000; + break; + case 'legendary': + baseFreq = 1200; + break; + } + + oscillator.frequency.setValueAtTime(baseFreq, audioContext.currentTime); + oscillator.frequency.setValueAtTime(baseFreq + 200, audioContext.currentTime + 0.1); + oscillator.frequency.setValueAtTime(baseFreq + 400, audioContext.currentTime + 0.2); + + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + } catch (error) { + console.log('Audio not available for achievement sound'); + } + + // Trigger haptic feedback + if ('vibrate' in navigator) { + const vibrationPattern = nextAchievement.rarity === 'legendary' ? [100, 50, 100, 50, 100] : [100, 50, 100]; + navigator.vibrate(vibrationPattern); + } + + // Set timeout to hide achievement and process next in queue + setTimeout(() => { + setIsVisible(false); + setTimeout(() => { + setCurrentAchievement(null); + setIsProcessingQueue(false); + // Process next achievement in queue after a short delay + setTimeout(processQueue, 100); + }, 500); + }, 6000); + }, [isProcessingQueue, achievementQueue, isVisible]); + + // Effect to start processing queue when new items are added + React.useEffect(() => { + if (achievementQueue.length > 0 && !isProcessingQueue && !isVisible) { + processQueue(); + } + }, [achievementQueue, isProcessingQueue, isVisible, processQueue]); + + const showAchievement = useCallback((achievementId, customAchievement = null) => { + let achievement; + + if (customAchievement) { + achievement = customAchievement; + } else if (ACHIEVEMENTS[achievementId]) { + achievement = ACHIEVEMENTS[achievementId]; + } else { + console.warn(`Achievement with id "${achievementId}" not found`); + return; + } + + // Check if achievement was already unlocked + if (achievement.id && unlockedAchievements.has(achievement.id)) { + console.log(`Achievement "${achievement.title}" already unlocked`); + return; + } + + // Mark achievement as unlocked + if (achievement.id) { + setUnlockedAchievements(prev => new Set([...prev, achievement.id])); + } + + // Add XP to total + setTotalXP(prev => prev + (achievement.xp || 0)); + + // Add achievement to queue + setAchievementQueue(prev => [...prev, achievement]); + }, [unlockedAchievements]); + + const hideAchievement = useCallback(() => { + setIsVisible(false); + setTimeout(() => { + setCurrentAchievement(null); + setIsProcessingQueue(false); + // Process next achievement in queue after a short delay + setTimeout(processQueue, 100); + }, 500); + }, [processQueue]); + + const createCustomAchievement = useCallback((title, description, icon = '🎉', xp = 100, rarity = 'common') => { + return { + title, + description, + icon, + xp, + rarity + }; + }, []); + + const getAchievementsByRarity = useCallback((rarity) => { + return Object.values(ACHIEVEMENTS).filter(achievement => achievement.rarity === rarity); + }, []); + + const getProgress = useCallback(() => { + const totalAchievements = Object.keys(ACHIEVEMENTS).length; + const unlockedCount = unlockedAchievements.size; + return { + unlocked: unlockedCount, + total: totalAchievements, + percentage: Math.round((unlockedCount / totalAchievements) * 100) + }; + }, [unlockedAchievements]); + + return { + currentAchievement, + isVisible, + totalXP, + unlockedAchievements: Array.from(unlockedAchievements), + achievementQueue, + queueLength: achievementQueue.length, + isProcessingQueue, + showAchievement, + hideAchievement, + createCustomAchievement, + getAchievementsByRarity, + getProgress, + ACHIEVEMENTS + }; +}; diff --git a/website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.jsx b/website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.jsx new file mode 100644 index 00000000000..2d2298f26d1 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.jsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect } from 'react'; +import LessonProgress from './LessonProgress'; +import LessonOutline from './LessonOutline'; +import QuestionCard from './QuestionCard'; +import './InteractiveLessonComponent.scss'; + +// Mock API data - in real implementation this would come from an API +const mockLessonData = { + id: 'react-fundamentals-1', + title: 'React Fundamentals - Lesson 1', + description: 'Learn the fundamentals of React development and component creation', + totalLessons: 8, + currentLesson: 1, + progress: 13, // percentage + outline: [ + { id: 1, title: 'Introduction to React', completed: true, active: true }, + { id: 2, title: 'Components & Props', completed: false, active: false }, + { id: 3, title: 'State Management', completed: false, active: false }, + { id: 4, title: 'Event Handling', completed: false, active: false }, + { id: 5, title: 'Conditional Rendering', completed: false, active: false }, + { id: 6, title: 'Lists & Keys', completed: false, active: false }, + { id: 7, title: 'Forms in React', completed: false, active: false }, + { id: 8, title: 'Advanced Patterns', completed: false, active: false } + ], + content: { + title: 'Introduction to React', + body: ` +

CONTENT

+

Introduction to React

+

This is where the main lesson content would be displayed. You could include text, images, videos, interactive examples, and other educational materials.

+

The content area is designed to be flexible and can accommodate various types of learning materials to help students understand the concepts being taught.

+ ` + }, + review: { + question: { + id: 'q1', + text: 'Which of the following is the correct way to create a React component?', + choices: [ + { id: 'choice1', text: 'function Component() { return
Hello
; }', correct: true }, + { id: 'choice2', text: 'const Component = () =>
Hello
;', correct: false }, + { id: 'choice3', text: 'class Component extends React.Component { render() { return
Hello
; } }', correct: false }, + { id: 'choice4', text: 'All of the above', correct: false } + ] + } + } +}; + +const InteractiveLessonComponent = ({ + lessonId, + onLessonComplete, + onProgressUpdate, + className = '' +}) => { + const [lessonData, setLessonData] = useState(null); + const [selectedChoice, setSelectedChoice] = useState(null); + const [showFeedback, setShowFeedback] = useState(false); + const [isAnswered, setIsAnswered] = useState(false); + const [currentStep, setCurrentStep] = useState('content'); // 'content' or 'review' + + // Simulate API call to fetch lesson data + useEffect(() => { + const fetchLessonData = async () => { + // In a real implementation, this would be an actual API call + // For now, we'll use the mock data + await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay + setLessonData(mockLessonData); + }; + + fetchLessonData(); + }, [lessonId]); + + const handleChoiceSelect = (choiceId) => { + if (isAnswered) return; + setSelectedChoice(choiceId); + }; + + const handleAnswerSubmit = () => { + if (!selectedChoice || isAnswered) return; + + setIsAnswered(true); + setShowFeedback(true); + + const selectedChoiceData = lessonData.review.question.choices.find( + choice => choice.id === selectedChoice + ); + + if (selectedChoiceData?.correct) { + // Correct answer - progress to next lesson + setTimeout(() => { + const newProgress = Math.min(lessonData.progress + 12.5, 100); + onProgressUpdate?.(newProgress); + + if (newProgress >= 100) { + onLessonComplete?.(lessonData.id); + } + }, 2000); + } + }; + + const handleNext = () => { + if (currentStep === 'content') { + setCurrentStep('review'); + } else if (isAnswered) { + // Move to next lesson or complete + const selectedChoiceData = lessonData.review.question.choices.find( + choice => choice.id === selectedChoice + ); + + if (selectedChoiceData?.correct) { + onLessonComplete?.(lessonData.id); + } else { + // Reset for retry + setSelectedChoice(null); + setShowFeedback(false); + setIsAnswered(false); + setCurrentStep('content'); + } + } + }; + + const handlePrev = () => { + if (currentStep === 'review') { + setCurrentStep('content'); + setSelectedChoice(null); + setShowFeedback(false); + setIsAnswered(false); + } + }; + + if (!lessonData) { + return ( +
+
+

Loading lesson...

+
+ ); + } + + return ( +
+
+

{lessonData.title}

+ +
+ +
+
+
+ {currentStep === 'content' && ( +
+
+
+ )} + + {currentStep === 'review' && ( +
+

REVIEW AREA

+ +
+ )} +
+ +
+ + + {currentStep === 'content' && ( + + )} + + {currentStep === 'review' && !isAnswered && ( + + )} + + {currentStep === 'review' && isAnswered && ( + + )} +
+
+ +
+ +
+
+
+ ); +}; + +export default InteractiveLessonComponent; diff --git a/website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.scss b/website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.scss new file mode 100644 index 00000000000..c83b182d957 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/InteractiveLessonComponent.scss @@ -0,0 +1,211 @@ +// Interactive Lesson Component Styles +.interactive-lesson { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + + .lesson-header { + margin-bottom: 2rem; + text-align: center; + + .lesson-title { + font-size: 1.5rem; + font-weight: 600; + color: var(--ifm-color-content, #111827); + margin: 0 0 1.5rem 0; + } + } + + .lesson-layout { + display: grid; + grid-template-columns: 1fr 320px; + gap: 2rem; + align-items: start; + + @media (max-width: 968px) { + grid-template-columns: 1fr; + gap: 1.5rem; + + .lesson-sidebar { + order: -1; + } + } + } + + .lesson-main { + .lesson-content-area { + background-color: var(--ifm-card-background-color, #ffffff); + border: 2px solid var(--ifm-color-emphasis-200, #e5e7eb); + border-radius: 12px; + padding: 2rem; + margin-bottom: 1.5rem; + min-height: 400px; + + .content-section { + .lesson-content { + h3 { + font-size: 1.25rem; + font-weight: 700; + color: var(--ifm-color-content, #111827); + margin: 0 0 1rem 0; + text-align: center; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + h4 { + font-size: 1.1rem; + font-weight: 600; + color: var(--ifm-color-content, #111827); + margin: 0 0 1rem 0; + } + + p { + color: var(--ifm-color-content-secondary, #6b7280); + line-height: 1.6; + margin: 0 0 1rem 0; + + &:last-child { + margin-bottom: 0; + } + } + } + } + + .review-section { + .review-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--ifm-color-content, #111827); + margin: 0 0 2rem 0; + text-align: center; + text-transform: uppercase; + letter-spacing: 0.05em; + } + } + } + + .lesson-navigation { + display: flex; + justify-content: space-between; + gap: 1rem; + + .nav-btn { + padding: 0.75rem 1.5rem; + border: 2px solid var(--ifm-color-primary, #3b82f6); + background-color: transparent; + color: var(--ifm-color-primary, #3b82f6); + font-weight: 600; + font-size: 0.875rem; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 0.05em; + + &:hover:not(.disabled) { + background-color: var(--ifm-color-primary, #3b82f6); + color: white; + } + + &.disabled { + opacity: 0.5; + cursor: not-allowed; + border-color: var(--ifm-color-emphasis-300, #d1d5db); + color: var(--ifm-color-emphasis-600, #6b7280); + } + + &.submit-btn { + background-color: var(--ifm-color-primary, #3b82f6); + color: white; + + &:hover:not(.disabled) { + background-color: var(--ifm-color-primary-dark, #2563eb); + } + } + } + } + } + + .lesson-sidebar { + position: sticky; + top: 2rem; + } +} + +// Loading state +.interactive-lesson-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + + .loading-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--ifm-color-emphasis-200, #e5e7eb); + border-top: 3px solid var(--ifm-color-primary, #3b82f6); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; + } + + p { + color: var(--ifm-color-content-secondary, #6b7280); + margin: 0; + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +// Dark mode support +[data-theme='dark'] { + .interactive-lesson { + .lesson-header .lesson-title { + color: var(--ifm-color-content, #f9fafb); + } + + .lesson-main .lesson-content-area { + background-color: var(--ifm-color-emphasis-100, #1f2937); + border-color: var(--ifm-color-emphasis-300, #374151); + + .content-section .lesson-content { + h3, h4 { + color: var(--ifm-color-content, #f9fafb); + } + + p { + color: var(--ifm-color-content-secondary, #d1d5db); + } + } + + .review-section .review-title { + color: var(--ifm-color-content, #f9fafb); + } + } + + .lesson-navigation .nav-btn { + &.disabled { + border-color: var(--ifm-color-emphasis-400, #4b5563); + color: var(--ifm-color-emphasis-500, #6b7280); + } + } + } + + .interactive-lesson-loading { + .loading-spinner { + border-color: var(--ifm-color-emphasis-300, #4b5563); + border-top-color: var(--ifm-color-primary, #3b82f6); + } + + p { + color: var(--ifm-color-content-secondary, #9ca3af); + } + } +} diff --git a/website/src/components/InteractiveLessonComponent/LessonOutline.jsx b/website/src/components/InteractiveLessonComponent/LessonOutline.jsx new file mode 100644 index 00000000000..3a0472a9950 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/LessonOutline.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import './LessonOutline.scss'; + +const LessonOutline = ({ outline, progress }) => { + return ( +
+

LESSON OUTLINE

+
+ {outline.map((item) => ( +
+ {item.id}. + {item.title} + {item.completed && ( + + )} +
+ ))} +
+
+ ); +}; + +export default LessonOutline; diff --git a/website/src/components/InteractiveLessonComponent/LessonOutline.scss b/website/src/components/InteractiveLessonComponent/LessonOutline.scss new file mode 100644 index 00000000000..14e0e3a32cb --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/LessonOutline.scss @@ -0,0 +1,130 @@ +// Lesson Outline Component Styles +.lesson-outline { + background-color: var(--ifm-card-background-color, #ffffff); + border: 2px solid var(--ifm-color-emphasis-200, #e5e7eb); + border-radius: 12px; + padding: 1.5rem; + + .outline-title { + font-size: 0.875rem; + font-weight: 700; + color: var(--ifm-color-content, #111827); + margin: 0 0 1.5rem 0; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: center; + } + + .outline-list { + .outline-item { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + border-radius: 8px; + background-color: var(--ifm-color-emphasis-50, #f9fafb); + border: 1px solid var(--ifm-color-emphasis-200, #e5e7eb); + transition: all 0.2s ease; + + &:last-child { + margin-bottom: 0; + } + + &.active { + background-color: var(--ifm-color-primary-lightest, #dbeafe); + border-color: var(--ifm-color-primary-light, #93c5fd); + color: var(--ifm-color-primary-dark, #1e40af); + + .item-number { + background-color: var(--ifm-color-primary, #3b82f6); + color: white; + } + + .item-title { + font-weight: 600; + } + } + + &.completed { + background-color: var(--ifm-color-success-lightest, #dcfce7); + border-color: var(--ifm-color-success-light, #86efac); + + .item-number { + background-color: var(--ifm-color-success, #16a34a); + color: white; + } + + .completion-check { + color: var(--ifm-color-success, #16a34a); + font-weight: 700; + margin-left: auto; + } + } + + .item-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background-color: var(--ifm-color-emphasis-300, #d1d5db); + color: var(--ifm-color-content, #111827); + border-radius: 50%; + font-size: 0.75rem; + font-weight: 700; + margin-right: 0.75rem; + flex-shrink: 0; + } + + .item-title { + font-size: 0.875rem; + color: var(--ifm-color-content, #111827); + flex: 1; + min-width: 0; + } + } + } +} + +// Dark mode support +[data-theme='dark'] { + .lesson-outline { + background-color: var(--ifm-color-emphasis-100, #1f2937); + border-color: var(--ifm-color-emphasis-300, #374151); + + .outline-title { + color: var(--ifm-color-content, #f9fafb); + } + + .outline-list { + .outline-item { + background-color: var(--ifm-color-emphasis-200, #374151); + border-color: var(--ifm-color-emphasis-300, #4b5563); + + &.active { + background-color: var(--ifm-color-primary-darker, #1e3a8a); + border-color: var(--ifm-color-primary, #3b82f6); + color: var(--ifm-color-primary-lighter, #93c5fd); + } + + &.completed { + background-color: var(--ifm-color-success-darker, #14532d); + border-color: var(--ifm-color-success, #16a34a); + + .completion-check { + color: var(--ifm-color-success-light, #4ade80); + } + } + + .item-number { + background-color: var(--ifm-color-emphasis-400, #6b7280); + color: var(--ifm-color-content, #f9fafb); + } + + .item-title { + color: var(--ifm-color-content, #f9fafb); + } + } + } + } +} diff --git a/website/src/components/InteractiveLessonComponent/LessonProgress.jsx b/website/src/components/InteractiveLessonComponent/LessonProgress.jsx new file mode 100644 index 00000000000..436dcdf94a5 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/LessonProgress.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import './LessonProgress.scss'; + +const LessonProgress = ({ current, total, progress }) => { + return ( +
+
+

TOTAL PROGRESS

+
+
+
+
+ + {current} of {total} lessons completed + + + {progress}% + +
+
+
+ ); +}; + +export default LessonProgress; diff --git a/website/src/components/InteractiveLessonComponent/LessonProgress.scss b/website/src/components/InteractiveLessonComponent/LessonProgress.scss new file mode 100644 index 00000000000..1b6eb6e2c14 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/LessonProgress.scss @@ -0,0 +1,80 @@ +// Lesson Progress Component Styles +.lesson-progress { + background-color: var(--ifm-card-background-color, #ffffff); + border: 2px solid var(--ifm-color-emphasis-200, #e5e7eb); + border-radius: 12px; + padding: 1.5rem; + + .progress-header { + .progress-title { + font-size: 0.875rem; + font-weight: 700; + color: var(--ifm-color-content, #111827); + margin: 0 0 1rem 0; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: center; + } + + .progress-bar { + width: 100%; + height: 8px; + background-color: var(--ifm-color-emphasis-200, #e5e7eb); + border-radius: 4px; + overflow: hidden; + margin-bottom: 1rem; + + .progress-fill { + height: 100%; + background-color: var(--ifm-color-primary, #3b82f6); + border-radius: 4px; + transition: width 0.3s ease; + } + } + + .progress-stats { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + + .lessons-completed { + color: var(--ifm-color-content-secondary, #6b7280); + } + + .progress-percentage { + font-weight: 700; + color: var(--ifm-color-content, #111827); + font-size: 1.125rem; + } + } + } +} + +// Dark mode support +[data-theme='dark'] { + .lesson-progress { + background-color: var(--ifm-color-emphasis-100, #1f2937); + border-color: var(--ifm-color-emphasis-300, #374151); + + .progress-header { + .progress-title { + color: var(--ifm-color-content, #f9fafb); + } + + .progress-bar { + background-color: var(--ifm-color-emphasis-300, #4b5563); + } + + .progress-stats { + .lessons-completed { + color: var(--ifm-color-content-secondary, #d1d5db); + } + + .progress-percentage { + color: var(--ifm-color-content, #f9fafb); + } + } + } + } +} diff --git a/website/src/components/InteractiveLessonComponent/QuestionCard.jsx b/website/src/components/InteractiveLessonComponent/QuestionCard.jsx new file mode 100644 index 00000000000..4bfa2d16e1c --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/QuestionCard.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import './QuestionCard.scss'; + +const QuestionCard = ({ + question, + selectedChoice, + onChoiceSelect, + showFeedback, + isAnswered +}) => { + const getChoiceClassName = (choice) => { + let className = 'choice-item'; + + if (selectedChoice === choice.id) { + className += ' selected'; + } + + if (showFeedback && isAnswered) { + if (choice.correct) { + className += ' correct'; + } else if (selectedChoice === choice.id && !choice.correct) { + className += ' incorrect'; + } + } + + return className; + }; + + return ( +
+
+

QUESTION

+
+ +
+

{question.text}

+ +
+ {question.choices.map((choice) => ( + + ))} +
+ + {showFeedback && isAnswered && ( +
+ {question.choices.find(c => c.id === selectedChoice)?.correct ? ( +
+ + Correct! Well done. +
+ ) : ( +
+ + + Incorrect. The correct answer is highlighted above. + +
+ )} +
+ )} +
+
+ ); +}; + +export default QuestionCard; diff --git a/website/src/components/InteractiveLessonComponent/QuestionCard.scss b/website/src/components/InteractiveLessonComponent/QuestionCard.scss new file mode 100644 index 00000000000..0b7c9ebc287 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/QuestionCard.scss @@ -0,0 +1,233 @@ +// Question Card Component Styles +.question-card { + .question-header { + margin-bottom: 1.5rem; + + .question-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--ifm-color-content, #111827); + margin: 0; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: center; + } + } + + .question-content { + .question-text { + font-size: 1rem; + font-weight: 500; + color: var(--ifm-color-content, #111827); + margin: 0 0 2rem 0; + line-height: 1.5; + text-align: center; + } + + .choices-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 2rem; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } + + .choice-item { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 1rem; + border: 2px solid var(--ifm-color-emphasis-200, #e5e7eb); + border-radius: 8px; + background-color: var(--ifm-card-background-color, #ffffff); + cursor: pointer; + transition: all 0.2s ease; + text-align: left; + width: 100%; + + &:hover:not(:disabled) { + border-color: var(--ifm-color-primary, #3b82f6); + background-color: var(--ifm-color-primary-lightest, #f0f9ff); + } + + &.selected { + border-color: var(--ifm-color-primary, #3b82f6); + background-color: var(--ifm-color-primary-lightest, #dbeafe); + + .choice-label { + color: var(--ifm-color-primary-dark, #1e40af); + } + } + + &.correct { + border-color: var(--ifm-color-success, #16a34a); + background-color: var(--ifm-color-success-lightest, #dcfce7); + + .choice-label { + color: var(--ifm-color-success-dark, #15803d); + } + } + + &.incorrect { + border-color: var(--ifm-color-danger, #ef4444); + background-color: var(--ifm-color-danger-lightest, #fef2f2); + + .choice-label { + color: var(--ifm-color-danger-dark, #dc2626); + } + } + + &:disabled { + cursor: not-allowed; + } + + .choice-label { + font-size: 0.75rem; + font-weight: 700; + color: var(--ifm-color-content-secondary, #6b7280); + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .choice-text { + font-size: 0.875rem; + color: var(--ifm-color-content, #111827); + line-height: 1.4; + font-family: 'Monaco', 'Consolas', 'Courier New', monospace; + background-color: var(--ifm-color-emphasis-50, #f9fafb); + padding: 0.5rem; + border-radius: 4px; + width: 100%; + box-sizing: border-box; + } + } + } + + .feedback-section { + display: flex; + justify-content: center; + + .feedback { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.5rem; + border-radius: 8px; + font-weight: 500; + + &.correct-feedback { + background-color: var(--ifm-color-success-lightest, #dcfce7); + border: 1px solid var(--ifm-color-success-light, #86efac); + color: var(--ifm-color-success-dark, #15803d); + + .feedback-icon { + color: var(--ifm-color-success, #16a34a); + font-weight: 700; + } + } + + &.incorrect-feedback { + background-color: var(--ifm-color-danger-lightest, #fef2f2); + border: 1px solid var(--ifm-color-danger-light, #fca5a5); + color: var(--ifm-color-danger-dark, #dc2626); + + .feedback-icon { + color: var(--ifm-color-danger, #ef4444); + font-weight: 700; + } + } + + .feedback-text { + font-size: 0.875rem; + } + } + } + } +} + +// Dark mode support +[data-theme='dark'] { + .question-card { + .question-header .question-title { + color: var(--ifm-color-content, #f9fafb); + } + + .question-content { + .question-text { + color: var(--ifm-color-content, #f9fafb); + } + + .choices-grid { + .choice-item { + border-color: var(--ifm-color-emphasis-300, #4b5563); + background-color: var(--ifm-color-emphasis-100, #1f2937); + + &:hover:not(:disabled) { + border-color: var(--ifm-color-primary, #3b82f6); + background-color: var(--ifm-color-primary-darker, #1e3a8a); + } + + &.selected { + background-color: var(--ifm-color-primary-darker, #1e3a8a); + + .choice-label { + color: var(--ifm-color-primary-lighter, #93c5fd); + } + } + + &.correct { + background-color: var(--ifm-color-success-darker, #14532d); + + .choice-label { + color: var(--ifm-color-success-light, #4ade80); + } + } + + &.incorrect { + background-color: var(--ifm-color-danger-darker, #7f1d1d); + + .choice-label { + color: var(--ifm-color-danger-light, #f87171); + } + } + + .choice-label { + color: var(--ifm-color-content-secondary, #d1d5db); + } + + .choice-text { + color: var(--ifm-color-content, #f9fafb); + background-color: var(--ifm-color-emphasis-200, #374151); + } + } + } + + .feedback-section { + .feedback { + &.correct-feedback { + background-color: var(--ifm-color-success-darker, #14532d); + border-color: var(--ifm-color-success, #16a34a); + color: var(--ifm-color-success-light, #4ade80); + + .feedback-icon { + color: var(--ifm-color-success-light, #4ade80); + } + } + + &.incorrect-feedback { + background-color: var(--ifm-color-danger-darker, #7f1d1d); + border-color: var(--ifm-color-danger, #ef4444); + color: var(--ifm-color-danger-light, #f87171); + + .feedback-icon { + color: var(--ifm-color-danger-light, #f87171); + } + } + } + } + } + } +} diff --git a/website/src/components/InteractiveLessonComponent/index.js b/website/src/components/InteractiveLessonComponent/index.js new file mode 100644 index 00000000000..403e25e7fe4 --- /dev/null +++ b/website/src/components/InteractiveLessonComponent/index.js @@ -0,0 +1,11 @@ +// Main component exports +export { default as InteractiveLessonComponent } from './InteractiveLessonComponent'; +export { default as LessonProgress } from './LessonProgress'; +export { default as LessonOutline } from './LessonOutline'; +export { default as QuestionCard } from './QuestionCard'; + +// Re-export everything for convenience +export * from './InteractiveLessonComponent'; +export * from './LessonProgress'; +export * from './LessonOutline'; +export * from './QuestionCard'; diff --git a/website/src/components/MultiStep/MultiStep.jsx b/website/src/components/MultiStep/MultiStep.jsx new file mode 100644 index 00000000000..72a54a24ce3 --- /dev/null +++ b/website/src/components/MultiStep/MultiStep.jsx @@ -0,0 +1,249 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; +import ProgressBar from './ProgressBar'; +import NavigationBar from './NavigationBar'; +import clsx from 'clsx'; +import './MultiStep.scss'; + +/** + * MultiStep component for creating multi-step forms with React Hook Form integration + * @param {Object} props - Component props + * @param {Array} props.steps - Array of step configurations + * @param {Function} props.onSubmit - Function called when form is submitted + * @param {Function} [props.onStepChange] - Callback for step changes + * @param {string} [props.className=''] - Additional CSS classes + * @param {boolean} [props.showProgressBar=true] - Whether to show the progress bar + * @param {boolean} [props.showStepNumbers=true] - Whether to show step numbers in progress + * @param {boolean} [props.allowSkip=false] - Allow skipping steps without validation + * @param {Object} [props.form] - External React Hook Form instance + * @param {Object} [props.defaultValues] - Default form values + */ +function MultiStep({ + steps, + onSubmit, + onStepChange, + className = '', + showProgressBar = true, + showStepNumbers = true, + allowSkip = false, + form: externalForm, + defaultValues +}) { + const [currentStep, setCurrentStep] = useState(1); + const [isSubmitting, setIsSubmitting] = useState(false); + const [stepErrors, setStepErrors] = useState({}); + + // Use external form if provided, otherwise create internal form + const internalForm = useForm({ + defaultValues: defaultValues, + mode: 'onChange' + }); + + const form = externalForm || internalForm; + const { trigger, handleSubmit, formState: { errors } } = form; + + // Calculate total steps + const totalSteps = steps.length; + + // Get current step configuration + const currentStepConfig = steps[currentStep - 1]; + + // Validate current step + const validateCurrentStep = useCallback(async () => { + if (!currentStepConfig.validation) return true; + + try { + await trigger(); + const hasStepErrors = Object.keys(errors).length > 0; + + setStepErrors(prev => ({ + ...prev, + [currentStep]: hasStepErrors + })); + + return !hasStepErrors; + } catch (error) { + console.error('Step validation error:', error); + setStepErrors(prev => ({ + ...prev, + [currentStep]: true + })); + return false; + } + }, [currentStep, currentStepConfig.validation, trigger, errors]); + + // Handle step navigation + const handleNext = useCallback(async () => { + if (currentStep === totalSteps) return; + + // Validate current step before proceeding + const isStepValid = await validateCurrentStep(); + + if (!isStepValid && !allowSkip) { + return; + } + + const nextStep = currentStep + 1; + setCurrentStep(nextStep); + onStepChange?.(nextStep, 'next'); + }, [currentStep, totalSteps, validateCurrentStep, allowSkip, onStepChange]); + + const handlePrev = useCallback(() => { + if (currentStep === 1) return; + + const prevStep = currentStep - 1; + setCurrentStep(prevStep); + onStepChange?.(prevStep, 'prev'); + }, [currentStep, onStepChange]); + + // Handle form submission + const handleFormSubmit = useCallback(async (data) => { + setIsSubmitting(true); + + try { + await onSubmit(data); + } catch (error) { + console.error('Form submission error:', error); + } finally { + setIsSubmitting(false); + } + }, [onSubmit]); + + // Handle final submit button click + const handleSubmitClick = useCallback(async () => { + // Validate current step first + const isStepValid = await validateCurrentStep(); + + if (!isStepValid) { + return; + } + + // Submit the form + const submitHandler = handleSubmit(handleFormSubmit); + submitHandler(); + }, [validateCurrentStep, handleSubmit, handleFormSubmit]); + + // Check if next button should be disabled + const isNextDisabled = !currentStepConfig.optional && !allowSkip && stepErrors[currentStep]; + + // Auto-validate when step changes + useEffect(() => { + validateCurrentStep(); + }, [currentStep, validateCurrentStep]); + + return ( + +
+ {/* Form Content */} +
+
+ {/* Step Header */} +
+
+
+

+ {currentStepConfig.title} +

+ {currentStepConfig.description && ( +

+ {currentStepConfig.description} +

+ )} +
+
+ Step {currentStep} of {totalSteps} +
+
+
+ + {/* Progress Bar */} + {showProgressBar && ( +
+ +
+ )} + + {/* Step Content */} +
+
+ {currentStepConfig.content} +
+ + {/* Step validation errors */} + {stepErrors[currentStep] && Object.keys(errors).length > 0 && ( +
+
+
+ + + +
+
+

+ Please fix the following errors: +

+
+
    + {Object.entries(errors).map(([field, error]) => ( +
  • + {String(error?.message) || `${field} is required`} +
  • + ))} +
+
+
+
+
+ )} +
+ + {/* Navigation Bar */} + +
+
+ + {/* Step Status Indicators */} +
+ {steps.map((_, index) => { + const stepNumber = index + 1; + const isActive = stepNumber === currentStep; + const isCompleted = stepNumber < currentStep; + const hasError = stepErrors[stepNumber]; + + return ( +
+ ); + })} +
+
+ + ); +} + +export default MultiStep; diff --git a/website/src/components/MultiStep/MultiStep.scss b/website/src/components/MultiStep/MultiStep.scss new file mode 100644 index 00000000000..a6d25a2823d --- /dev/null +++ b/website/src/components/MultiStep/MultiStep.scss @@ -0,0 +1,268 @@ +// MultiStep component styles +.multistep-container { + width: 100%; + max-width: 56rem; + margin: 0 auto; + padding: 0 1rem; +} + +.multistep-form { + background-color: var(--ifm-card-background-color, #ffffff); + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + border: 1px solid var(--ifm-color-emphasis-200, #e5e7eb); + overflow: hidden; + transition: all 0.3s ease; +} + +.multistep-header { + padding: 2rem 2.5rem 1.5rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200, #e5e7eb); + background-color: var(--ifm-color-emphasis-50, #f9fafb); + + .multistep-header-content { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.5rem; + } + + .multistep-title-section { + flex: 1; + min-width: 0; + } + + .multistep-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--ifm-color-content, #111827); + margin: 0 0 0.5rem 0; + line-height: 1.3; + } + + .multistep-description { + font-size: 1rem; + color: var(--ifm-color-content-secondary, #6b7280); + margin: 0; + line-height: 1.5; + } + + .multistep-counter { + font-size: 0.875rem; + font-weight: 500; + color: var(--ifm-color-content-secondary, #6b7280); + background-color: var(--ifm-color-emphasis-100, #f3f4f6); + padding: 0.5rem 1rem; + border-radius: 6px; + white-space: nowrap; + } +} + +.multistep-progress-section { + padding: 1.5rem 2.5rem 0; + background-color: var(--ifm-card-background-color, #ffffff); + border-bottom: 1px solid var(--ifm-color-emphasis-200, #e5e7eb); +} + +.multistep-content { + padding: 2.5rem; + min-height: 20rem; + + .multistep-step-content { + transition: opacity 0.2s ease-in-out; + } +} + +.multistep-error-container { + margin-top: 2rem; + padding: 1.25rem; + background-color: var(--ifm-color-danger-contrast-background, #fef2f2); + border: 1px solid var(--ifm-color-danger-contrast-foreground, #fecaca); + border-radius: 8px; + + .multistep-error-content { + display: flex; + align-items: flex-start; + gap: 1rem; + } + + .multistep-error-icon { + flex-shrink: 0; + margin-top: 0.125rem; + + .error-icon { + height: 1.25rem; + width: 1.25rem; + color: var(--ifm-color-danger, #ef4444); + } + } + + .multistep-error-text { + flex: 1; + + .multistep-error-title { + font-size: 1rem; + font-weight: 600; + color: var(--ifm-color-danger-dark, #991b1b); + margin: 0 0 0.75rem 0; + } + + .multistep-error-list { + font-size: 0.875rem; + color: var(--ifm-color-danger-dark, #b91c1c); + + ul { + list-style-type: disc; + list-style-position: inside; + margin: 0; + padding: 0; + + li { + margin-bottom: 0.5rem; + line-height: 1.4; + + &:last-child { + margin-bottom: 0; + } + } + } + } + } +} + +.multistep-indicators { + margin-top: 1.5rem; + display: flex; + justify-content: center; + gap: 0.75rem; + + .multistep-indicator { + width: 10px; + height: 10px; + border-radius: 50%; + transition: all 0.2s ease-in-out; + border: 2px solid transparent; + + &--active { + background-color: var(--ifm-color-primary, #3b82f6); + border-color: var(--ifm-color-primary-lighter, #93c5fd); + transform: scale(1.2); + } + + &--completed { + background-color: var(--ifm-color-success, #16a34a); + border-color: var(--ifm-color-success-lighter, #86efac); + } + + &--error { + background-color: var(--ifm-color-danger, #ef4444); + border-color: var(--ifm-color-danger-lighter, #fca5a5); + } + + &--inactive { + background-color: var(--ifm-color-emphasis-300, #d1d5db); + border-color: var(--ifm-color-emphasis-200, #e5e7eb); + } + } +} + +// Responsive design +@media (max-width: 768px) { + .multistep-container { + padding: 0 0.75rem; + } + + .multistep-header { + padding: 1.5rem 1.5rem 1rem; + + .multistep-header-content { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .multistep-title { + font-size: 1.25rem; + } + + .multistep-counter { + align-self: flex-end; + } + } + + .multistep-progress-section { + padding: 1rem 1.5rem 0; + } + + .multistep-content { + padding: 1.5rem; + } + + .multistep-error-container { + margin-top: 1.5rem; + padding: 1rem; + } +} + +// Dark mode support +[data-theme='dark'] { + .multistep-form { + background-color: var(--ifm-card-background-color, #1f2937); + border-color: var(--ifm-color-emphasis-300, #374151); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + } + + .multistep-header { + background-color: var(--ifm-color-emphasis-100, #111827); + border-color: var(--ifm-color-emphasis-300, #374151); + + .multistep-title { + color: var(--ifm-color-content, #f9fafb); + } + + .multistep-description { + color: var(--ifm-color-content-secondary, #9ca3af); + } + + .multistep-counter { + color: var(--ifm-color-content-secondary, #9ca3af); + background-color: var(--ifm-color-emphasis-200, #374151); + } + } + + .multistep-progress-section { + background-color: var(--ifm-card-background-color, #1f2937); + border-color: var(--ifm-color-emphasis-300, #374151); + } + + .multistep-error-container { + background-color: var(--ifm-color-danger-contrast-background, #450a0a); + border-color: var(--ifm-color-danger-contrast-foreground, #dc2626); + + .multistep-error-title { + color: var(--ifm-color-danger-lighter, #fca5a5); + } + + .multistep-error-list { + color: var(--ifm-color-danger-light, #f87171); + } + + .error-icon { + color: var(--ifm-color-danger, #dc2626); + } + } + + .multistep-indicators { + .multistep-indicator { + &--inactive { + background-color: var(--ifm-color-emphasis-400, #6b7280); + border-color: var(--ifm-color-emphasis-300, #4b5563); + } + + &--completed { + background-color: #22c55e; + border-color: #4ade80; + } + } + } +} diff --git a/website/src/components/MultiStep/MultiStepExample.jsx b/website/src/components/MultiStep/MultiStepExample.jsx new file mode 100644 index 00000000000..9be5f2099d8 --- /dev/null +++ b/website/src/components/MultiStep/MultiStepExample.jsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import MultiStep from './MultiStep'; +import './MultiStepExample.scss'; + +/** + * MultiStepExample component demonstrating usage of the MultiStep component + */ +const MultiStepExample = () => { + const form = useForm({ + defaultValues: { + personalInfo: { + name: '', + email: '', + age: 0 + }, + preferences: { + interests: [], + experience: '' + }, + feedback: { + comments: '', + rating: 5 + } + } + }); + + const steps = [ + { + id: 'personal', + title: 'Personal Information', + description: 'Tell us about yourself', + content: ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) + }, + { + id: 'preferences', + title: 'Preferences', + description: 'Select your interests and experience level', + content: ( +
+
+ +
+ {['Technology', 'Design', 'Business', 'Science', 'Arts'].map((interest) => ( + + ))} +
+
+
+ + +
+
+ ) + }, + { + id: 'feedback', + title: 'Feedback', + description: 'Share your thoughts and rate your experience', + content: ( +
+
+ +