fix: align learn schema with existing LearnCourse/LearnModule models
- Remove duplicate Course/Module models (GitHub main already has LearnCourse/LearnModule) - Add seed-learn.json with Daily Fiqh for Beginners (5 modules) - Add seed-learn.js using prisma.learnCourse / prisma.learnModule - Seed includes quizData, keyTakeaway, duration, giteaPath - Matches existing API routes: /api/learn/courses, /api/learn/tts, /api/learn/progress TODO: - Add markdown content to modules (currently content field is null) - Generate TTS audio files for listen toggle - Add intermediate/advanced courses
This commit is contained in:
@@ -489,3 +489,4 @@ model LearnCertificate {
|
|||||||
// Add relations to User model
|
// Add relations to User model
|
||||||
/// NOTE: The Notification and Referral relations are declared on the models above.
|
/// NOTE: The Notification and Referral relations are declared on the models above.
|
||||||
/// User now implicitly has: notifications Notification[], referredReferrals Referral[] (via "Referrer"), referrerReferral Referral? (via "Referred")
|
/// User now implicitly has: notifications Notification[], referredReferrals Referral[] (via "Referrer"), referrerReferral Referral? (via "Referred")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Seed script for micro-learning courses.
|
||||||
|
* Reads prisma/seed-learn.json and creates LearnCourse/LearnModule records.
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* npx prisma db push
|
||||||
|
* node prisma/seed-learn.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const seedPath = path.join(__dirname, 'seed-learn.json');
|
||||||
|
const raw = fs.readFileSync(seedPath, 'utf-8');
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
|
console.log(`Seeding ${data.courses.length} course(s)...`);
|
||||||
|
|
||||||
|
for (const course of data.courses) {
|
||||||
|
const { modules, ...courseData } = course;
|
||||||
|
|
||||||
|
// Upsert course (match by slug)
|
||||||
|
const upserted = await prisma.learnCourse.upsert({
|
||||||
|
where: { slug: courseData.slug },
|
||||||
|
update: {
|
||||||
|
title: courseData.title,
|
||||||
|
author: courseData.author,
|
||||||
|
description: courseData.description,
|
||||||
|
difficulty: courseData.difficulty,
|
||||||
|
imageUrl: courseData.imageUrl,
|
||||||
|
giteaPath: courseData.giteaPath,
|
||||||
|
priceFlh: courseData.priceFlh,
|
||||||
|
priceUsd: courseData.priceUsd,
|
||||||
|
subscriptionOnly: courseData.subscriptionOnly,
|
||||||
|
published: courseData.published,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
slug: courseData.slug,
|
||||||
|
title: courseData.title,
|
||||||
|
author: courseData.author,
|
||||||
|
description: courseData.description,
|
||||||
|
difficulty: courseData.difficulty,
|
||||||
|
imageUrl: courseData.imageUrl,
|
||||||
|
giteaPath: courseData.giteaPath,
|
||||||
|
priceFlh: courseData.priceFlh,
|
||||||
|
priceUsd: courseData.priceUsd,
|
||||||
|
subscriptionOnly: courseData.subscriptionOnly,
|
||||||
|
published: courseData.published,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` Course: ${upserted.title} (${upserted.id})`);
|
||||||
|
|
||||||
|
// Delete old modules (clean re-seed for dev)
|
||||||
|
await prisma.learnModule.deleteMany({ where: { courseId: upserted.id } });
|
||||||
|
|
||||||
|
// Create modules
|
||||||
|
if (modules && modules.length > 0) {
|
||||||
|
const totalMinutes = modules.reduce((sum, m) => sum + (m.duration || 5), 0);
|
||||||
|
|
||||||
|
for (const m of modules) {
|
||||||
|
await prisma.learnModule.create({
|
||||||
|
data: {
|
||||||
|
courseId: upserted.id,
|
||||||
|
order: m.order,
|
||||||
|
title: m.title,
|
||||||
|
slug: m.slug,
|
||||||
|
keyTakeaway: m.keyTakeaway,
|
||||||
|
duration: m.duration || 5,
|
||||||
|
content: m.content || null,
|
||||||
|
audioPath: m.audioPath || null,
|
||||||
|
quizData:
|
||||||
|
typeof m.quizData === 'string'
|
||||||
|
? m.quizData
|
||||||
|
: JSON.stringify(m.quizData || []),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update course with derived fields
|
||||||
|
await prisma.learnCourse.update({
|
||||||
|
where: { id: upserted.id },
|
||||||
|
data: {
|
||||||
|
moduleCount: modules.length,
|
||||||
|
totalMinutes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` → ${modules.length} module(s) created (${totalMinutes} min total)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Seeding complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"courses": [
|
||||||
|
{
|
||||||
|
"slug": "daily-fiqh-beginner",
|
||||||
|
"title": "Daily Fiqh for Beginners",
|
||||||
|
"author": "FalahMobile Learning",
|
||||||
|
"description": "Essential rulings for everyday Muslim life — from waking up to going to bed. Short lessons you can listen to during your commute or while making breakfast.",
|
||||||
|
"imageUrl": "/images/courses/daily-fiqh-beginner.jpg",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"giteaPath": "courses/daily-fiqh-beginner",
|
||||||
|
"priceFlh": null,
|
||||||
|
"priceUsd": null,
|
||||||
|
"subscriptionOnly": false,
|
||||||
|
"published": true,
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"order": 1,
|
||||||
|
"title": "The Intention of Wudu",
|
||||||
|
"slug": "intention-of-wudu",
|
||||||
|
"keyTakeaway": "Wudu only counts if you intend it. The intention is the aim of the heart, not words on the tongue.",
|
||||||
|
"duration": 2,
|
||||||
|
"audioPath": null,
|
||||||
|
"quizData": [
|
||||||
|
{
|
||||||
|
"question": "Where does the intention for wudu need to be made?",
|
||||||
|
"options": ["Before touching water", "While washing the face", "After finishing wudu", "Loudly, so others can hear"],
|
||||||
|
"correctIndex": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 2,
|
||||||
|
"title": "How to Perform Wudu Step by Step",
|
||||||
|
"slug": "how-to-perform-wudu",
|
||||||
|
"keyTakeaway": "Four acts in order: face, arms, head, feet. Each with tranquility. The Quran describes wudu with elegant precision.",
|
||||||
|
"duration": 3,
|
||||||
|
"audioPath": null,
|
||||||
|
"quizData": [
|
||||||
|
{
|
||||||
|
"question": "How many essential steps (pillars) does salah have?",
|
||||||
|
"options": ["5", "7", "10", "14"],
|
||||||
|
"correctIndex": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 3,
|
||||||
|
"title": "When Wudu Breaks",
|
||||||
|
"slug": "when-wudu-breaks",
|
||||||
|
"keyTakeaway": "Eight things break wudu. If you are unsure, assume you did not break it. Build on certainty, not suspicion.",
|
||||||
|
"duration": 2,
|
||||||
|
"audioPath": null,
|
||||||
|
"quizData": [
|
||||||
|
{
|
||||||
|
"question": "If you are unsure whether you broke wudu, what should you assume?",
|
||||||
|
"options": ["Assume you broke it and make wudu again", "Assume you did not break it and continue", "Ask a friend what they think", "Skip prayer to be safe"],
|
||||||
|
"correctIndex": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 4,
|
||||||
|
"title": "The Call to Prayer",
|
||||||
|
"slug": "the-call-to-prayer",
|
||||||
|
"keyTakeaway": "The adhan is an invitation from Allah. Repeat after the muezzin, send blessings on the Prophet, and make dua.",
|
||||||
|
"duration": 2,
|
||||||
|
"audioPath": null,
|
||||||
|
"quizData": [
|
||||||
|
{
|
||||||
|
"question": "What should you do immediately after hearing the adhan?",
|
||||||
|
"options": ["Rush to the prayer mat", "Repeat what the muezzin says", "Start eating if you were about to break your fast", "Change into clean clothes"],
|
||||||
|
"correctIndex": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 5,
|
||||||
|
"title": "The Essentials of Salah",
|
||||||
|
"slug": "essentials-of-salah",
|
||||||
|
"keyTakeaway": "Salah has 14 pillars. Missing any intentionally invalidates the prayer. Tranquility over speed.",
|
||||||
|
"duration": 3,
|
||||||
|
"audioPath": null,
|
||||||
|
"quizData": [
|
||||||
|
{
|
||||||
|
"question": "Which surah must be recited in every rak'ah of every prayer?",
|
||||||
|
"options": ["Surah al-Ikhlas", "Surah al-Fatiha", "Surah al-Baqarah", "Any surah the worshipper chooses"],
|
||||||
|
"correctIndex": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user