From fc4ed81e7c2649a4692e4082efa6f9d9de9b8a53 Mon Sep 17 00:00:00 2001 From: wmj2024 Date: Sun, 28 Jun 2026 04:35:08 +0800 Subject: [PATCH] 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 --- prisma/schema.prisma | 1 + prisma/seed-learn.js | 108 +++++++++++++++++++++++++++++++++++++++++ prisma/seed-learn.json | 94 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 prisma/seed-learn.js create mode 100644 prisma/seed-learn.json diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 29e9e4f..b4cd312 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -489,3 +489,4 @@ model LearnCertificate { // Add relations to User model /// 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") + diff --git a/prisma/seed-learn.js b/prisma/seed-learn.js new file mode 100644 index 0000000..05d8b85 --- /dev/null +++ b/prisma/seed-learn.js @@ -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(); + }); diff --git a/prisma/seed-learn.json b/prisma/seed-learn.json new file mode 100644 index 0000000..751c0db --- /dev/null +++ b/prisma/seed-learn.json @@ -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 + } + ] + } + ] + } + ] +}