diff --git a/backend/main.py b/backend/main.py index de381ed..c62f4ac 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,11 +1,13 @@ import os from pathlib import Path -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pydantic import BaseModel, ConfigDict from typing import List, Optional, Dict, Any +from services.tutorial_service import TutorialService +from models.tutorial_models import Tutorial import uvicorn app = FastAPI( @@ -14,6 +16,31 @@ version="1.0.0" ) +# Tutorial routes +@app.get("/api/tutorials", response_model=List[Tutorial]) +async def get_tutorials( + difficulty: Optional[str] = Query(None, description="Filter tutorials by difficulty level"), + category: Optional[str] = Query(None, description="Filter tutorials by category") +): + """ + Get all tutorials with optional filtering by difficulty or category. + """ + if difficulty: + return TutorialService.get_tutorials_by_difficulty(difficulty) + elif category: + return TutorialService.get_tutorials_by_category(category) + return TutorialService.get_all_tutorials() + +@app.get("/api/tutorials/{tutorial_id}", response_model=Tutorial) +async def get_tutorial(tutorial_id: str): + """ + Get a specific tutorial by ID. + """ + tutorial = TutorialService.get_tutorial_by_id(tutorial_id) + if not tutorial: + raise HTTPException(status_code=404, detail="Tutorial not found") + return tutorial + # Environment detection ENVIRONMENT = os.getenv("ENVIRONMENT", "development") IS_PRODUCTION = ENVIRONMENT == "production" diff --git a/backend/models/tutorial_models.py b/backend/models/tutorial_models.py new file mode 100644 index 0000000..402a6a7 --- /dev/null +++ b/backend/models/tutorial_models.py @@ -0,0 +1,31 @@ +from typing import List, Optional +from pydantic import BaseModel + +class QuizOption(BaseModel): + text: str + isCorrect: bool + +class Quiz(BaseModel): + question: str + options: List[QuizOption] + explanation: Optional[str] = None + +class TutorialStep(BaseModel): + id: str + type: str # 'explanation', 'quiz', 'interactive' + title: str + content: str + quiz: Optional[Quiz] = None + codeExample: Optional[str] = None + interactive: bool = False + +class Tutorial(BaseModel): + id: str + title: str + description: str + difficulty: str # 'Beginner', 'Intermediate', 'Advanced' + estimatedTime: str + prerequisites: List[str] + steps: List[TutorialStep] + category: str # 'sorting', 'graph', 'string', 'dp' + icon: str diff --git a/backend/services/tutorial_service.py b/backend/services/tutorial_service.py new file mode 100644 index 0000000..ac3fc3a --- /dev/null +++ b/backend/services/tutorial_service.py @@ -0,0 +1,95 @@ +from typing import Dict, List, Optional +from models.tutorial_models import Tutorial, TutorialStep, Quiz, QuizOption + +# Mock database of tutorials +TUTORIALS_DB: Dict[str, Tutorial] = { + "sorting": Tutorial( + id="sorting", + title="Sorting Algorithms Mastery", + description="Learn about different sorting algorithms and their implementations", + difficulty="Beginner", + estimatedTime="30 minutes", + prerequisites=["basic-arrays"], + category="sorting", + icon="🔄", + steps=[ + TutorialStep( + id="intro", + type="explanation", + title="Introduction to Sorting", + content="Sorting is a fundamental operation in computer science...", + interactive=True + ), + TutorialStep( + id="bubble-sort", + type="explanation", + title="Bubble Sort Algorithm", + content="Bubble Sort is the simplest sorting algorithm...", + codeExample="""def bubble_sort(arr): + n = len(arr) + for i in range(n): + for j in range(0, n-i-1): + if arr[j] > arr[j+1]: + arr[j], arr[j+1] = arr[j+1], arr[j] + return arr""", + interactive=True, + quiz=Quiz( + question="What is the time complexity of Bubble Sort?", + options=[ + QuizOption(text="O(n)", isCorrect=False), + QuizOption(text="O(n²)", isCorrect=True), + QuizOption(text="O(n log n)", isCorrect=False), + QuizOption(text="O(1)", isCorrect=False) + ], + explanation="Bubble Sort uses nested loops, leading to O(n²) time complexity" + ) + ) + ] + ), + "graph": Tutorial( + id="graph", + title="Graph Algorithms", + description="Master graph traversal and pathfinding algorithms", + difficulty="Intermediate", + estimatedTime="45 minutes", + prerequisites=["basic-arrays", "sorting"], + category="graph", + icon="🕸️", + steps=[ + TutorialStep( + id="intro", + type="explanation", + title="Introduction to Graphs", + content="Graphs are versatile data structures...", + interactive=True + ) + ] + ) +} + +class TutorialService: + @staticmethod + def get_all_tutorials() -> List[Tutorial]: + """Get all available tutorials.""" + return list(TUTORIALS_DB.values()) + + @staticmethod + def get_tutorial_by_id(tutorial_id: str) -> Optional[Tutorial]: + """Get a specific tutorial by ID.""" + return TUTORIALS_DB.get(tutorial_id) + + @staticmethod + def get_tutorials_by_difficulty(difficulty: str) -> List[Tutorial]: + """Get tutorials filtered by difficulty level.""" + return [ + tutorial for tutorial in TUTORIALS_DB.values() + if tutorial.difficulty.lower() == difficulty.lower() + ] + + @staticmethod + def get_tutorials_by_category(category: str) -> List[Tutorial]: + """Get tutorials filtered by category.""" + return [ + tutorial for tutorial in TUTORIALS_DB.values() + if tutorial.category.lower() == category.lower() + ] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6979e26..4d57b46 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,6 +30,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", + "react-syntax-highlighter": "^15.5.0", "tailwindcss": "^3.2.7", "use-sound": "^4.0.1" }, @@ -4879,6 +4880,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5138,6 +5148,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -6851,6 +6867,36 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chart.js": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", @@ -7110,6 +7156,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -9790,6 +9846,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -10187,6 +10256,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10694,6 +10771,33 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -10703,6 +10807,21 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -11161,6 +11280,30 @@ "node": ">= 10" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -11322,6 +11465,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -11409,6 +11562,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -14900,6 +15063,20 @@ "tslib": "^2.0.3" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -15703,6 +15880,24 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -17294,6 +17489,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -17339,6 +17543,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -17819,6 +18036,23 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.6", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -17913,6 +18147,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -18939,6 +19197,16 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -21340,6 +21608,15 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index becc672..4bab14b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", + "react-syntax-highlighter": "^15.5.0", "tailwindcss": "^3.2.7", "use-sound": "^4.0.1" }, diff --git a/frontend/src/App.js b/frontend/src/App.js index c9c0fcd..3eb8d47 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -11,7 +11,11 @@ import HomePage from './pages/Home'; import AboutPage from './pages/AboutPage'; import DocumentationPage from './pages/DocumentationPage'; import ContributorsPage from './pages/ContributorsPage'; +import TutorialPage from './pages/TutorialPage'; +import TutorialsPage from './pages/TutorialsPage'; + import NotFoundPage from './pages/NotFoundPage'; + import './App.css'; const AppContent = () => { @@ -58,6 +62,14 @@ const AppContent = () => { path="*" element={} /> + } + /> + } + /> diff --git a/frontend/src/components/Layout/Navbar.jsx b/frontend/src/components/Layout/Navbar.jsx index 4a434f4..edcb3b6 100644 --- a/frontend/src/components/Layout/Navbar.jsx +++ b/frontend/src/components/Layout/Navbar.jsx @@ -17,6 +17,7 @@ const Navbar = () => { { path: '/graph', label: 'Graph', icon: Network }, { path: '/string', label: 'String', icon: Type }, { path: '/dp', label: 'DP', icon: Layers }, + { path: '/tutorials', label: 'Tutorials', icon: BookOpen }, { path: '/about', label: 'About', icon: Info }, { path: '/docs', label: 'Docs', icon: BookOpen } ]; diff --git a/frontend/src/components/Tutorial/TutorialLayout.jsx b/frontend/src/components/Tutorial/TutorialLayout.jsx new file mode 100644 index 0000000..aef439f --- /dev/null +++ b/frontend/src/components/Tutorial/TutorialLayout.jsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect } from 'react'; +import TutorialStep from './TutorialStep'; +import TutorialProgress from './TutorialProgress'; +import TutorialQuiz from './TutorialQuiz'; + +const TutorialLayout = ({ tutorialId }) => { + const [currentTutorial, setCurrentTutorial] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [progress, setProgress] = useState({}); + + useEffect(() => { + // Load tutorial data + const loadTutorial = async () => { + try { + const tutorialData = await getTutorialById(tutorialId); + setCurrentTutorial(tutorialData); + } catch (error) { + console.error('Error loading tutorial:', error); + // Handle error appropriately + } + }; + + loadTutorial(); + loadProgress(); + }, [tutorialId]); + + const loadProgress = () => { + const savedProgress = localStorage.getItem('tutorialProgress'); + if (savedProgress) { + setProgress(JSON.parse(savedProgress)); + } + }; + + const saveProgress = (updatedProgress) => { + localStorage.setItem('tutorialProgress', JSON.stringify(updatedProgress)); + setProgress(updatedProgress); + }; + + const handleStepComplete = () => { + const updatedProgress = { + ...progress, + [tutorialId]: { + ...progress[tutorialId], + completedSteps: [...(progress[tutorialId]?.completedSteps || []), currentStep] + } + }; + saveProgress(updatedProgress); + setCurrentStep(currentStep + 1); + }; + + if (!currentTutorial) { + return
Loading tutorial...
; + } + + const currentStepData = currentTutorial.steps[currentStep]; + + return ( +
+
+
+

+ {currentTutorial.title} +

+
+ Estimated time: {currentTutorial.estimatedTime} +
+
+ +
+
+ {currentStepData.type === "explanation" && ( + + )} + {currentStepData.quiz && ( + + )} +
+
+ +
+
+
+
+ ); +}; + +export default TutorialLayout; diff --git a/frontend/src/components/Tutorial/TutorialProgress.jsx b/frontend/src/components/Tutorial/TutorialProgress.jsx new file mode 100644 index 0000000..a921003 --- /dev/null +++ b/frontend/src/components/Tutorial/TutorialProgress.jsx @@ -0,0 +1,56 @@ +import React from 'react'; + +const TutorialProgress = ({ tutorial, currentStep, progress }) => { + const calculateProgress = () => { + if (!progress?.completedSteps) return 0; + return (progress.completedSteps.length / tutorial.steps.length) * 100; + }; + + const progressPercentage = calculateProgress(); + + return ( +
+

Your Progress

+
+
+
+
+

+ {Math.round(progressPercentage)}% Complete +

+
+
+ {tutorial.steps.map((step, index) => ( +
+
+ {progress?.completedSteps?.includes(index) ? '✓' : index + 1} +
+ + {step.title || `Step ${index + 1}`} + +
+ ))} +
+
+ ); +}; + +export default TutorialProgress; diff --git a/frontend/src/components/Tutorial/TutorialQuiz.jsx b/frontend/src/components/Tutorial/TutorialQuiz.jsx new file mode 100644 index 0000000..201ab98 --- /dev/null +++ b/frontend/src/components/Tutorial/TutorialQuiz.jsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; + +const TutorialQuiz = ({ quiz, onComplete }) => { + const [selectedAnswer, setSelectedAnswer] = useState(null); + const [showFeedback, setShowFeedback] = useState(false); + const [isCorrect, setIsCorrect] = useState(false); + + const handleAnswerSelect = (index) => { + setSelectedAnswer(index); + }; + + const handleSubmit = () => { + if (selectedAnswer === null) return; + + const correct = selectedAnswer === quiz.correct; + setIsCorrect(correct); + setShowFeedback(true); + + if (correct) { + setTimeout(() => { + onComplete(); + }, 1500); + } + }; + + return ( +
+

Quiz Time!

+
+

{quiz.question}

+
+
+ {quiz.options.map((option, index) => ( + + ))} +
+ {!showFeedback && ( + + )} + {showFeedback && ( +
+ {isCorrect ? 'Correct! Well done!' : 'Try again! Review the material and give it another shot.'} +
+ )} +
+ ); +}; + +export default TutorialQuiz; diff --git a/frontend/src/components/Tutorial/TutorialStep.jsx b/frontend/src/components/Tutorial/TutorialStep.jsx new file mode 100644 index 0000000..2e197ef --- /dev/null +++ b/frontend/src/components/Tutorial/TutorialStep.jsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +const TutorialStep = ({ step, onComplete }) => { + const [isCompleted, setIsCompleted] = useState(false); + + const handleComplete = () => { + setIsCompleted(true); + onComplete(); + }; + + const renderContent = () => { + if (step.codeExample) { + return ( +
+ + {step.codeExample} + +
+ ); + } + return
{step.content}
; + }; + + return ( +
+

+ {step.title} +

+ {renderContent()} + {step.interactive && !isCompleted && ( +
+ +
+ )} +
+ ); +}; + +export default TutorialStep; diff --git a/frontend/src/pages/TutorialPage.jsx b/frontend/src/pages/TutorialPage.jsx new file mode 100644 index 0000000..0f2a264 --- /dev/null +++ b/frontend/src/pages/TutorialPage.jsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import TutorialLayout from '../components/Tutorial/TutorialLayout'; +import { getTutorial } from '../services/tutorialService'; + +const TutorialPage = () => { + const { tutorialId } = useParams(); + const navigate = useNavigate(); + const [tutorial, setTutorial] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const loadTutorial = async () => { + try { + const tutorialData = await getTutorial(tutorialId); + if (!tutorialData) { + setError('Tutorial not found'); + return; + } + setTutorial(tutorialData); + } catch (err) { + setError('Failed to load tutorial'); + } + }; + + loadTutorial(); + }, [tutorialId]); + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ); + } + + if (!tutorial) { + return ( +
+
+
+
+
+
+
+ ); + } + + return ; +}; + +export default TutorialPage; diff --git a/frontend/src/pages/TutorialsPage.jsx b/frontend/src/pages/TutorialsPage.jsx new file mode 100644 index 0000000..de43b54 --- /dev/null +++ b/frontend/src/pages/TutorialsPage.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +const TutorialsPage = ({ darkMode }) => { + const navigate = useNavigate(); + const [tutorials, setTutorials] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchTutorials = async () => { + try { + const data = await getTutorials(); + setTutorials(data); + } catch (err) { + setError('Failed to load tutorials'); + console.error('Error fetching tutorials:', err); + } finally { + setLoading(false); + } + }; + + fetchTutorials(); + }, []); + + const difficultyColors = { + Beginner: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', + Intermediate: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', + Advanced: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' + }; + + return ( +
+
+

+ Interactive Tutorials +

+

+ Choose a tutorial to start learning algorithms through interactive visualizations +

+
+ +
+ {tutorials.map((tutorial) => ( +
navigate(`/tutorial/${tutorial.id}`)} + > +
+
+ {tutorial.icon} +
+

+ {tutorial.title} +

+ + {tutorial.difficulty} + +
+
+

+ {tutorial.description} +

+
+ ⏱️ {tutorial.duration} +
+
+
+ ))} +
+
+ ); +}; + +export default TutorialsPage; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index f541d43..dcfc6bb 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -322,6 +322,43 @@ function generateFallbackSteps(algorithm, graphData) { return { steps }; } +// Tutorial API endpoints +export const getTutorials = async (filters = {}) => { + const { difficulty, category } = filters; + const params = new URLSearchParams(); + if (difficulty) params.append('difficulty', difficulty); + if (category) params.append('category', category); + + try { + const response = await axios.get(`${getApiBaseUrl()}/api/tutorials?${params.toString()}`); + return response.data; + } catch (error) { + console.error('Error fetching tutorials:', error); + throw error; + } +}; + +export const getTutorialById = async (tutorialId) => { + try { + const response = await axios.get(`${getApiBaseUrl()}/api/tutorials/${tutorialId}`); + return response.data; + } catch (error) { + console.error(`Error fetching tutorial ${tutorialId}:`, error); + throw error; + } +}; + +// Update the tutorial service to use the API +export const updateTutorialProgress = async (tutorialId, progress) => { + // Store progress locally for now + // In a real application, this would be an API call + const existingProgress = localStorage.getItem('tutorialProgress'); + const allProgress = existingProgress ? JSON.parse(existingProgress) : {}; + allProgress[tutorialId] = progress; + localStorage.setItem('tutorialProgress', JSON.stringify(allProgress)); + return progress; +}; + console.log(`🔗 API Base URL: ${API_BASE_URL}`); console.log(`🏗️ Environment: ${process.env.NODE_ENV}`); diff --git a/frontend/src/services/tutorialService.js b/frontend/src/services/tutorialService.js new file mode 100644 index 0000000..d6e3511 --- /dev/null +++ b/frontend/src/services/tutorialService.js @@ -0,0 +1,113 @@ +// Tutorial service to manage tutorial data and progress +const tutorialData = { + 'sorting': { + id: 'sorting', + title: 'Sorting Algorithms Mastery', + difficulty: 'Beginner', + prerequisites: ['basic-arrays'], + estimatedTime: '30 minutes', + steps: [ + { + id: 'intro', + title: 'Introduction to Sorting', + type: 'explanation', + content: 'Sorting algorithms are fundamental building blocks in computer science...', + interactive: true + }, + { + id: 'bubble-sort', + title: 'Bubble Sort', + type: 'explanation', + content: 'Bubble sort is a simple sorting algorithm...', + codeExample: `function bubbleSort(arr) { + const n = arr.length; + for (let i = 0; i < n - 1; i++) { + for (let j = 0; j < n - i - 1; j++) { + if (arr[j] > arr[j + 1]) { + // Swap elements + [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; + } + } + } + return arr; +}`, + interactive: true, + quiz: { + question: 'What is the time complexity of bubble sort?', + options: ['O(n)', 'O(n²)', 'O(n log n)', 'O(1)'], + correct: 1 + } + } + ] + }, + 'graph': { + id: 'graph', + title: 'Graph Algorithms', + difficulty: 'Intermediate', + prerequisites: ['basic-arrays', 'sorting'], + estimatedTime: '45 minutes', + steps: [ + { + id: 'intro', + title: 'Introduction to Graphs', + type: 'explanation', + content: 'Graphs are versatile data structures...', + interactive: true + } + ] + } +}; + +export const getTutorial = async (id) => { + // Simulate API call + return new Promise((resolve) => { + setTimeout(() => { + resolve(tutorialData[id]); + }, 500); + }); +}; + +export const getTutorialProgress = (tutorialId) => { + const progress = localStorage.getItem('tutorialProgress'); + if (progress) { + const allProgress = JSON.parse(progress); + return allProgress[tutorialId]; + } + return null; +}; + +export const saveTutorialProgress = (tutorialId, progress) => { + const existingProgress = localStorage.getItem('tutorialProgress'); + const allProgress = existingProgress ? JSON.parse(existingProgress) : {}; + + allProgress[tutorialId] = progress; + localStorage.setItem('tutorialProgress', JSON.stringify(allProgress)); +}; + +export const generateCertificate = (tutorialId) => { + const progress = getTutorialProgress(tutorialId); + if (!progress || !tutorialData[tutorialId]) { + return null; + } + + const tutorial = tutorialData[tutorialId]; + const completionPercentage = (progress.completedSteps.length / tutorial.steps.length) * 100; + + if (completionPercentage < 100) { + return null; + } + + return { + tutorialTitle: tutorial.title, + completionDate: new Date().toISOString(), + difficulty: tutorial.difficulty, + studentName: 'Student Name' // TODO: Add user management + }; +}; + +export default { + getTutorial, + getTutorialProgress, + saveTutorialProgress, + generateCertificate +};