diff --git a/app/Models/Lesson.php b/app/Models/Lesson.php index 4b8f7a4..b8e1031 100644 --- a/app/Models/Lesson.php +++ b/app/Models/Lesson.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Auth; final class Lesson extends Model { @@ -57,6 +58,21 @@ public function userProgress(): HasMany return $this->hasMany(UserProgress::class); } + /** + * Accessor: Dynamically check if the *currently authenticated* user + * has completed this lesson. + */ + public function getIsCompletedAttribute(): bool + { + // If no user is logged in, it's not completed by them + if (! Auth::check()) { + return false; + } + + // Check if a progress record exists for the logged-in user and this lesson + return $this->userProgress()->where('user_id', Auth::id())->exists(); + } + // A Lesson has many external resources. public function externalResources(): HasMany { diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 18fb540..af7e70d 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -14,13 +14,22 @@ public function up(): void { Schema::create('users', function (Blueprint $table) { + $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); - $table->enum('preferred_learning_style', ['reading', 'visual'])->default('reading')->nullable()->after('password'); - $table->foreignId('learning_path_id')->nullable()->after('preferred_learning_style')->constrained('learning_paths')->onDelete('set null'); + $table->enum('preferred_learning_style', ['reading', 'visual', 'balanced']) + ->default('balanced') + ->nullable() // Allow null if user hasn't set it + ->after('password'); // Place it after password + + $table->foreignId('learning_path_id') // New column name + ->nullable() + ->after('preferred_learning_style') + ->constrained('learning_paths') // Foreign key to learning_paths table + ->onDelete('set null'); $table->timestamps(); }); diff --git a/database/migrations/2025_04_27_192057_create_lessons_table.php b/database/migrations/2025_04_17_141934_create_lessons_table.php similarity index 84% rename from database/migrations/2025_04_27_192057_create_lessons_table.php rename to database/migrations/2025_04_17_141934_create_lessons_table.php index 5571829..350c883 100644 --- a/database/migrations/2025_04_27_192057_create_lessons_table.php +++ b/database/migrations/2025_04_17_141934_create_lessons_table.php @@ -22,7 +22,8 @@ public function up(): void $table->text('video_embed_html')->nullable()->after('content'); $table->text('assignment')->nullable()->comment('Assignment description'); $table->text('initial_code')->nullable()->comment('Starting code for editor'); - $table->text('expected_output')->nullable(); + // $table->text('solution')->nullable()->comment('Optional solution code/explanation'); + $table->text('expected_output')->nullable()->comment('For simple stdout checks'); // Example addition $table->unsignedSmallInteger('order')->default(0); $table->timestamps(); diff --git a/database/migrations/2025_04_27_192057_create_modules_table.php b/database/migrations/2025_04_17_141934_create_modules_table.php similarity index 100% rename from database/migrations/2025_04_27_192057_create_modules_table.php rename to database/migrations/2025_04_17_141934_create_modules_table.php diff --git a/database/migrations/2025_04_27_192057_create_courses_table.php b/database/migrations/2025_04_17_142012_create_courses_table.php similarity index 89% rename from database/migrations/2025_04_27_192057_create_courses_table.php rename to database/migrations/2025_04_17_142012_create_courses_table.php index 7f6e0f8..edc1ec2 100644 --- a/database/migrations/2025_04_27_192057_create_courses_table.php +++ b/database/migrations/2025_04_17_142012_create_courses_table.php @@ -19,6 +19,7 @@ public function up(): void $table->string('slug')->unique(); $table->text('description')->nullable(); $table->boolean('is_published')->default(false); + // Set null on delete: if the quiz is deleted, the course doesn't break, just loses its assessment link. $table->foreignId('assessment_quiz_id')->nullable()->after('is_published')->constrained('quizzes')->onDelete('set null'); $table->foreignId('final_review_quiz_id')->nullable()->after('assessment_quiz_id')->constrained('quizzes')->onDelete('set null'); $table->timestamps(); diff --git a/database/migrations/2025_04_28_131921_create_quizzes_table.php b/database/migrations/2025_04_22_192107_create_quizzes_table.php similarity index 100% rename from database/migrations/2025_04_28_131921_create_quizzes_table.php rename to database/migrations/2025_04_22_192107_create_quizzes_table.php diff --git a/database/migrations/2025_04_28_131922_create_questions_table.php b/database/migrations/2025_04_22_192108_create_questions_table.php similarity index 85% rename from database/migrations/2025_04_28_131922_create_questions_table.php rename to database/migrations/2025_04_22_192108_create_questions_table.php index 87f3be1..caa6325 100644 --- a/database/migrations/2025_04_28_131922_create_questions_table.php +++ b/database/migrations/2025_04_22_192108_create_questions_table.php @@ -16,7 +16,10 @@ public function up(): void Schema::create('questions', function (Blueprint $table) { $table->id(); $table->foreignId('quiz_id')->constrained()->onDelete('cascade'); + // Crucial link for suggesting review topics! + // Can be null if a question is general, or set null if lesson deleted. $table->foreignId('lesson_id')->nullable()->constrained()->onDelete('set null'); + // Start with multiple choice, add more later if needed $table->enum('type', ['multiple_choice', 'true_false', 'fill_blank'])->default('multiple_choice'); $table->text('text')->comment('The question text'); $table->json('options')->nullable(); diff --git a/database/migrations/2025_04_28_131922_create_quiz_answers_table.php b/database/migrations/2025_04_22_192108_create_quiz_answers_table.php similarity index 79% rename from database/migrations/2025_04_28_131922_create_quiz_answers_table.php rename to database/migrations/2025_04_22_192108_create_quiz_answers_table.php index b749613..439336f 100644 --- a/database/migrations/2025_04_28_131922_create_quiz_answers_table.php +++ b/database/migrations/2025_04_22_192108_create_quiz_answers_table.php @@ -17,11 +17,12 @@ public function up(): void $table->id(); $table->foreignId('quiz_attempt_id')->constrained()->onDelete('cascade'); $table->foreignId('question_id')->constrained()->onDelete('cascade'); + // Store the user's selected option ID (e.g., "a", "b") $table->string('user_answer')->nullable(); - $table->boolean('is_correct')->nullable(); + $table->boolean('is_correct')->nullable()->comment('Graded result'); $table->timestamps(); - // User can answer each question once per attempt + // User should only answer each question once per attempt $table->unique(['quiz_attempt_id', 'question_id']); }); } diff --git a/database/migrations/2025_04_28_131922_create_quiz_attempts_table.php b/database/migrations/2025_04_22_192108_create_quiz_attempts_table.php similarity index 85% rename from database/migrations/2025_04_28_131922_create_quiz_attempts_table.php rename to database/migrations/2025_04_22_192108_create_quiz_attempts_table.php index 6683832..7c0f5c5 100644 --- a/database/migrations/2025_04_28_131922_create_quiz_attempts_table.php +++ b/database/migrations/2025_04_22_192108_create_quiz_attempts_table.php @@ -18,9 +18,10 @@ public function up(): void $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->foreignId('quiz_id')->nullable()->constrained()->onDelete('cascade'); $table->string('type')->default('standard')->after('quiz_id'); + // Score as percentage (0-100), calculated after submission $table->unsignedTinyInteger('score')->nullable(); $table->timestamp('started_at')->useCurrent(); - $table->timestamp('completed_at')->nullable(); + $table->timestamp('completed_at')->nullable(); // Set when submitted $table->timestamps(); }); } diff --git a/database/migrations/2025_04_28_131923_create_lesson_quiz_table.php b/database/migrations/2025_04_22_192603_create_lesson_quiz_table.php similarity index 91% rename from database/migrations/2025_04_28_131923_create_lesson_quiz_table.php rename to database/migrations/2025_04_22_192603_create_lesson_quiz_table.php index a251c24..2152708 100644 --- a/database/migrations/2025_04_28_131923_create_lesson_quiz_table.php +++ b/database/migrations/2025_04_22_192603_create_lesson_quiz_table.php @@ -17,7 +17,7 @@ public function up(): void $table->id(); $table->foreignId('lesson_id')->constrained()->onDelete('cascade'); $table->foreignId('quiz_id')->constrained()->onDelete('cascade'); - $table->timestamps(); + $table->timestamps(); // Optional, but good practice $table->unique(['lesson_id', 'quiz_id']); }); diff --git a/database/migrations/2025_05_21_181615_create_user_progress_table.php b/database/migrations/2025_04_24_114041_create_user_progress_table.php similarity index 78% rename from database/migrations/2025_05_21_181615_create_user_progress_table.php rename to database/migrations/2025_04_24_114041_create_user_progress_table.php index ade2eda..d3182a9 100644 --- a/database/migrations/2025_05_21_181615_create_user_progress_table.php +++ b/database/migrations/2025_04_24_114041_create_user_progress_table.php @@ -15,12 +15,13 @@ public function up(): void { Schema::create('user_progress', function (Blueprint $table) { $table->id(); + // Foreign keys linking to users and lessons $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->foreignId('lesson_id')->constrained()->onDelete('cascade'); - $table->timestamp('completed_at')->useCurrent(); + $table->timestamp('completed_at')->useCurrent(); // Record when completed $table->timestamps(); - // A user can complete a specific lesson only once. + // IMPORTANT: A user can complete a specific lesson only once $table->unique(['user_id', 'lesson_id']); }); } diff --git a/database/migrations/2025_05_22_184048_create_learning_paths_table.php b/database/migrations/2025_05_17_165334_create_learning_paths_table.php similarity index 100% rename from database/migrations/2025_05_22_184048_create_learning_paths_table.php rename to database/migrations/2025_05_17_165334_create_learning_paths_table.php diff --git a/database/migrations/2025_05_22_184102_create_learning_path_course_table.php b/database/migrations/2025_05_17_170453_create_learning_path_course_table.php similarity index 72% rename from database/migrations/2025_05_22_184102_create_learning_path_course_table.php rename to database/migrations/2025_05_17_170453_create_learning_path_course_table.php index b9a5376..6206d5d 100644 --- a/database/migrations/2025_05_22_184102_create_learning_path_course_table.php +++ b/database/migrations/2025_05_17_170453_create_learning_path_course_table.php @@ -17,10 +17,10 @@ public function up(): void $table->id(); $table->foreignId('learning_path_id')->constrained()->onDelete('cascade'); $table->foreignId('course_id')->constrained()->onDelete('cascade'); - $table->unsignedSmallInteger('order')->default(0); - $table->timestamps(); + $table->unsignedSmallInteger('order')->default(0); // Order of the course in the path + $table->timestamps(); // Usually not needed for simple pivot unless tracking when added - $table->unique(['learning_path_id', 'course_id']); + $table->unique(['learning_path_id', 'course_id']); // A course appears once per path $table->index(['learning_path_id', 'order']); }); } diff --git a/database/migrations/2025_05_22_184123_create_external_resources_table.php b/database/migrations/2025_05_20_115956_create_external_resources_table.php similarity index 100% rename from database/migrations/2025_05_22_184123_create_external_resources_table.php rename to database/migrations/2025_05_20_115956_create_external_resources_table.php diff --git a/database/seeders/ExternalResourceSeeder.php b/database/seeders/ExternalResourceSeeder.php new file mode 100644 index 0000000..08e9d06 --- /dev/null +++ b/database/seeders/ExternalResourceSeeder.php @@ -0,0 +1,49 @@ +first(); + if ($lessonDeclare) { + ExternalResource::create([ + 'lesson_id' => $lessonDeclare->id, + 'title' => 'MDN: let keyword', + 'url' => 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let', + 'type' => 'documentation', + 'description' => 'Detailed documentation on the `let` keyword from Mozilla Developer Network.', + ]); + ExternalResource::create([ + 'lesson_id' => $lessonDeclare->id, + 'title' => 'Understanding var, let, and const in JavaScript (Video)', + 'url' => 'https://www.youtube.com/watch?v=s-hLgT_t3uA', // Example video + 'type' => 'video', + 'description' => 'A video explaining the differences and use cases for variable declarations.', + ]); + } + + $lessonTypes = Lesson::where('slug', 'primitive-data-types')->first(); + if ($lessonTypes) { + ExternalResource::create([ + 'lesson_id' => $lessonTypes->id, + 'title' => 'MDN: JavaScript data types and data structures', + 'url' => 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures', + 'type' => 'documentation', + ]); + } + // Add more resources for other lessons... + } +} diff --git a/database/seeders/LearningPathCourseSeeder.php b/database/seeders/LearningPathCourseSeeder.php new file mode 100644 index 0000000..5fe8586 --- /dev/null +++ b/database/seeders/LearningPathCourseSeeder.php @@ -0,0 +1,52 @@ +truncate(); + + $pathFrontend = LearningPath::where('slug', 'frontend-developer-path')->first(); + $pathBackend = LearningPath::where('slug', 'backend-javascript-developer-path')->first(); + $pathFullstack = LearningPath::where('slug', 'full-stack-javascript-path')->first(); + + $courseFundamentals = Course::where('slug', 'javascript-fundamentals')->first(); + $courseIntermediate = Course::where('slug', 'intermediate-web-dev-js')->first(); + $courseAdvanced = Course::where('slug', 'advanced-js-nodejs')->first(); + + if ($pathFrontend && $courseFundamentals) { + $pathFrontend->courses()->attach($courseFundamentals->id, ['order' => 1]); + } + if ($pathFrontend && $courseIntermediate) { + $pathFrontend->courses()->attach($courseIntermediate->id, ['order' => 2]); + } + + if ($pathBackend && $courseFundamentals) { + $pathBackend->courses()->attach($courseFundamentals->id, ['order' => 1]); + } + if ($pathBackend && $courseAdvanced) { + $pathBackend->courses()->attach($courseAdvanced->id, ['order' => 2]); + } + + if ($pathFullstack && $courseFundamentals) { + $pathFullstack->courses()->attach($courseFundamentals->id, ['order' => 1]); + } + if ($pathFullstack && $courseIntermediate) { + $pathFullstack->courses()->attach($courseIntermediate->id, ['order' => 2]); + } + if ($pathFullstack && $courseAdvanced) { + $pathFullstack->courses()->attach($courseAdvanced->id, ['order' => 3]); + } + } +} diff --git a/database/seeders/LearningPathSeeder.php b/database/seeders/LearningPathSeeder.php new file mode 100644 index 0000000..6187b85 --- /dev/null +++ b/database/seeders/LearningPathSeeder.php @@ -0,0 +1,41 @@ + 'Frontend Developer Path', + 'slug' => Str::slug('Frontend Developer Path'), + 'description' => 'Learn the essentials of frontend web development, focusing on JavaScript, HTML, CSS, and modern frameworks.', + 'is_active' => true, + ]); + + LearningPath::create([ + 'name' => 'Backend JavaScript Developer Path', + 'slug' => Str::slug('Backend JavaScript Developer Path'), + 'description' => 'Master server-side JavaScript with Node.js, Express, and databases to build robust APIs and web applications.', + 'is_active' => true, + ]); + + LearningPath::create([ + 'name' => 'Full-Stack JavaScript Path', + 'slug' => Str::slug('Full-Stack JavaScript Path'), + 'description' => 'Become proficient in both frontend and backend JavaScript technologies for a comprehensive skill set.', + 'is_active' => true, + ]); + } +} diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue index 78af1b4..ca14801 100644 --- a/resources/js/pages/Dashboard.vue +++ b/resources/js/pages/Dashboard.vue @@ -1,7 +1,8 @@ +
{{ nextSuggestedCourse.description }}
+ + Go to Course + ++ Congratulations! You've completed all available courses in this path. +
+Check back later for new courses or explore other learning paths!
+