Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/Models/Lesson.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down
13 changes: 11 additions & 2 deletions database/migrations/0001_01_01_000000_create_users_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
}
Expand Down
49 changes: 49 additions & 0 deletions database/seeders/ExternalResourceSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Database\Seeders;

use App\Models\ExternalResource;
use App\Models\Lesson;
use Illuminate\Database\Seeder;

final class ExternalResourceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
ExternalResource::truncate();

$lessonDeclare = Lesson::where('slug', 'declaring-variables')->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...
}
}
52 changes: 52 additions & 0 deletions database/seeders/LearningPathCourseSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Database\Seeders;

use App\Models\Course;
use App\Models\LearningPath;
use Illuminate\Database\Seeder;

final class LearningPathCourseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// DB::table('learning_path_course')->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]);
}
}
}
41 changes: 41 additions & 0 deletions database/seeders/LearningPathSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Database\Seeders;

use App\Models\LearningPath;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;

final class LearningPathSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
LearningPath::truncate();

LearningPath::create([
'name' => '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,
]);
}
}
Loading