Construa um aplicativo web full-stack de cronômetro para sala de aula inteligente.
De Wikiprompt, a enciclopédia livre de prompts
Construa um aplicativo web full-stack de cronômetro para sala de aula inteligente. نص برومبت شامل باللغة العربية لبناء تطبيق ويب كامل (Full-Stack) لساعة صفية تفاعلية ذكية، تتضمن عدّادًا تنازليًا، وأصواتًا، ومنتقيًا عشوائيًا للطلاب، وتخصيصًا للمراحل.
Conteúdo do PromptSalvar
🌐
خطة تطوير التطبيق:
1. **البنية التقنية**: تطبيق ويب كامل (Full-Stack) باستخدام Next.js 14 مع React و Tailwind CSS للتصميم، مع قاعدة بيانات SQLite عبر Prisma لتخزين أسماء الطلاب ومراحل الدرس.
2. **المكونات الرئيسية**:
- مكوّن المؤقت الدائري (Circular Timer) مع تغيير الألوان
- نظام الصوت (Web Audio API) مع مكتبة أصوات جاهزة
- أداة اختيار الطالب العشوائي
- مدير مراحل الدرس
3. **واجهة المستخدم**: تصميم عربي RTL بألوان مريحة (أزرق داكن، أبيض، وألوان متدرجة للمؤقت)
4. **قاعدة البيانات**: جدول للطلاب، جدول للمراحل، إعدادات التطبيق
---
سأبدأ الآن ببناء التطبيق:
```jsx
// app/layout.jsx
import './globals.css'
import { Cairo } from 'next/font/google'
const cairo = Cairo({ subsets: ['arabic'] })
export const metadata = {
title: 'مؤقت الفصل الذكي',
description: 'تطبيق مؤقت تفاعلي للمعلمين',
}
export default function RootLayout({ children }) {
return (
<html lang="ar" dir="rtl">
<body className={cairo.className}>{children}</body>
</html>
)
}
```
```jsx
// app/page.jsx
'use client'
import { useState, useEffect, useRef } from 'react'
import Timer from '@/components/Timer'
import StudentPicker from '@/components/StudentPicker'
import StageManager from '@/components/StageManager'
import SoundLibrary from '@/components/SoundLibrary'
import { PrismaClient } from '@prisma/client'
export default function Home() {
const [students, setStudents] = useState([])
const [stages, setStages] = useState([])
const [isMuted, setIsMuted] = useState(false)
const [currentStage, setCurrentStage] = useState(null)
useEffect(() => {
fetchStudents()
fetchStages()
}, [])
const fetchStudents = async () => {
const res = await fetch('/api/students')
const data = await res.json()
setStudents(data)
}
const fetchStages = async () => {
const res = await fetch('/api/stages')
const data = await res.json()
setStages(data)
}
return (
<main className="min-h-screen bg-gradient-to-br from-blue-900 to-purple-900 p-6">
<div className="max-w-7xl mx-auto">
<header className="text-center mb-8">
<h1 className="text-4xl font-bold text-white mb-2">مؤقت الفصل الذكي</h1>
<p className="text-blue-200">أداة تفاعلية لإدارة وقت الحصة الدراسية</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Timer
isMuted={isMuted}
currentStage={currentStage}
onStageComplete={() => setCurrentStage(null)}
/>
</div>
<div className="space-y-6">
<StudentPicker students={students} isMuted={isMuted} />
<SoundLibrary isMuted={isMuted} setIsMuted={setIsMuted} />
</div>
</div>
<div className="mt-8">
<StageManager
stages={stages}
setStages={setStages}
currentStage={currentStage}
setCurrentStage={setCurrentStage}
/>
</div>
</div>
</main>
)
}
```
```jsx
// components/Timer.jsx
'use client'
import { useState, useEffect, useRef } from 'react'
import { playSound } from '@/utils/sound'
export default function Timer({ isMuted, currentStage, onStageComplete }) {
const [duration, setDuration] = useState(45 * 60) // 45 minutes in seconds
const [timeLeft, setTimeLeft] = useState(45 * 60)
const [isRunning, setIsRunning] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const [lastMinutePlayed, setLastMinutePlayed] = useState(false)
const timerRef = useRef(null)
useEffect(() => {
if (currentStage) {
setDuration(currentStage.duration * 60)
setTimeLeft(currentStage.duration * 60)
setIsRunning(true)
setIsPaused(false)
}
}, [currentStage])
useEffect(() => {
if (isRunning && !isPaused) {
timerRef.current = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
clearInterval(timerRef.current)
setIsRunning(false)
if (!isMuted) playSound('timesUp')
onStageComplete()
return 0
}
// Last minute warning
if (prev === 60 && !lastMinutePlayed) {
setLastMinutePlayed(true)
if (!isMuted) playSound('lastMinute')
}
return prev - 1
})
}, 1000)
}
return () => clearInterval(timerRef.current)
}, [isRunning, isPaused, isMuted, lastMinutePlayed])
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
const getColor = () => {
const percentage = (timeLeft / duration) * 100
if (percentage > 50) return '#22c55e' // Green
if (percentage > 20) return '#eab308' // Yellow
return '#ef4444' // Red
}
const circumference = 2 * Math.PI * 120
const progress = ((duration - timeLeft) / duration) * circumference
const startTimer = () => {
if (timeLeft === 0) {
setTimeLeft(duration)
}
setIsRunning(true)
setIsPaused(false)
if (!isMuted) playSound('start')
}
const pauseTimer = () => {
setIsPaused(true)
setIsRunning(false)
}
const resetTimer = () => {
setIsRunning(false)
setIsPaused(false)
setTimeLeft(duration)
setLastMinutePlayed(false)
}
return (
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 shadow-2xl">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-white mb-2">
{currentStage ? currentStage.name : 'الدرس الكامل'}
</h2>
<p className="text-blue-200">
{currentStage ? `مدة المرحلة: ${currentStage.duration} دقيقة` : 'المدة الافتراضية: 45 دقيقة'}
</p>
</div>
<div className="relative w-72 h-72 mx-auto mb-8">
<svg className="w-full h-full -rotate-90" viewBox="0 0 280 280">
<circle
cx="140"
cy="140"
r="120"
fill="none"
stroke="#ffffff20"
strokeWidth="12"
/>
<circle
cx="140"
cy="140"
r="120"
fill="none"
stroke={getColor()}
strokeWidth="12"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={circumference - progress}
className="transition-all duration-1000"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-6xl font-bold text-white" style={{ direction: 'ltr' }}>
{formatTime(timeLeft)}
</span>
<span className="text-blue-200 mt-2">الوقت المتبقي</span>
</div>
</div>
<div className="flex justify-center gap-4">
{!isRunning && !isPaused && (
<button
onClick={startTimer}
className="px-8 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-bold transition-colors"
>
▶ بدء
</button>
)}
{isRunning && (
<button
onClick={pauseTimer}
className="px-8 py-3 bg-yellow-500 hover:bg-yellow-600 text-white rounded-xl font-bold transition-colors"
>
⏸ إيقاف مؤقت
</button>
)}
{isPaused && (
<button
onClick={() => { setIsRunning(true); setIsPaused(false) }}
className="px-8 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-bold transition-colors"
>
▶ متابعة
</button>
)}
<button
onClick={resetTimer}
className="px-8 py-3 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold transition-colors"
>
↺ إعادة تعيين
</button>
</div>
</div>
)
}
```
```jsx
// components/StudentPicker.jsx
'use client'
import { useState } from 'react'
import { playSound } from '@/utils/sound'
export default function StudentPicker({ students, isMuted }) {
const [selectedStudent, setSelectedStudent] = useState(null)
const [isPicking, setIsPicking] = useState(false)
const [showAddForm, setShowAddForm] = useState(false)
const [newStudentName, setNewStudentName] = useState('')
const pickRandomStudent = () => {
if (students.length === 0) return
setIsPicking(true)
if (!isMuted) playSound('suspense')
// Animation effect
let counter = 0
const interval = setInterval(() => {
const randomIndex = Math.floor(Math.random() * students.length)
setSelectedStudent(students[randomIndex])
counter++
if (counter > 15) {
clearInterval(interval)
setIsPicking(false)
if (!isMuted) playSound('ding')
}
}, 100)
}
const addStudent = async (e) => {
e.preventDefault()
if (!newStudentName.trim()) return
const res = await fetch('/api/students', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newStudentName.trim() })
})
if (res.ok) {
setNewStudentName('')
setShowAddForm(false)
window.location.reload()
}
}
return (
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 shadow-2xl">
<h3 className="text-xl font-bold text-white mb-4">🎯 اختيار طالب عشوائي</h3>
<button
onClick={pickRandomStudent}
disabled={isPicking || students.length === 0}
className="w-full py-4 bg-purple-500 hover:bg-purple-600 disabled:bg-gray-500 text-white rounded-xl font-bold transition-colors mb-4"
>
{isPicking ? 'جاري الاختيار...' : 'اختر طالباً'}
</button>
{selectedStudent && (
<div className="text-center py-6 bg-white/5 rounded-xl mb-4">
<p className="text-3xl font-bold text-yellow-300 animate-pulse">
{selectedStudent.name}
</p>
</div>
)}
<button
onClick={() => setShowAddForm(!showAddForm)}
className="w-full py-2 text-blue-300 hover:text-blue-200 transition-colors"
>
{showAddForm ? 'إغلاق' : '+ إضافة طالب جديد'}
</button>
{showAddForm && (
<form onSubmit={addStudent} className="mt-4">
<input
type="text"
value={newStudentName}
onChange={(e) => setNewStudentName(e.target.value)}
placeholder="اسم الطالب"
className="w-full px-4 py-2 bg-white/10 text-white rounded-xl placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-purple-500 mb-2"
/>
<button
type="submit"
className="w-full py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-xl font-bold transition-colors"
>
حفظ الطالب
</button>
</form>
)}
{students.length > 0 && (
<div className="mt-4 max-h-40 overflow-y-auto">
<p className="text-blue-200 text-sm mb-2">الطلاب المسجلون ({students.length}):</p>
<div className="flex flex-wrap gap-2">
{students.map((student) => (
<span key={student.id} className="px-3 py-1 bg-white/10 text-white rounded-full text-sm">
{student.name}
</span>
))}
</div>
</div>
)}
</div>
)
}
```
```jsx
// components/SoundLibrary.jsx
'use client'
import { playSound } from '@/utils/sound'
export default function SoundLibrary({ isMuted, setIsMuted }) {
const sounds = [
{ id: 'applause', name: '👏 تصفيق', icon: '👏' },
{ id: 'ding', name: '🔔 جرس نجاح', icon: '🔔' },
{ id: 'error', name: '❌ صوت خطأ', icon: '❌' },
{ id: 'music', name: '🎵 موسيقى قصيرة', icon: '🎵' },
{ id: 'start', name: '🚀 بداية الدرس', icon: '🚀' },
{ id: 'timesUp', name: '⏰ انتهاء الوقت', icon: '⏰' },
]
return (
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 shadow-2xl">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-white">🔊 مكتبة الأصوات</h3>
<button
onClick={() => setIsMuted(!isMuted)}
className={`px-4 py-2 rounded-xl font-bold transition-colors ${
isMuted ? 'bg-red-500 hover:bg-red-600' : 'bg-green-500 hover:bg-green-600'
} text-white`}
>
{isMuted ? '🔇 كتم' : '🔊 صوت'}
</button>
</div>
<div className="grid grid-cols-2 gap-3">
{sounds.map((sound) => (
<button
key={sound.id}
onClick={() => !isMuted && playSound(sound.id)}
disabled={isMuted}
className="p-3 bg-white/5 hover:bg-white/10 disabled:opacity-50 rounded-xl text-white transition-colors flex flex-col items-center gap-2"
>
<span className="text-2xl">{sound.icon}</span>
<span className="text-sm">{sound.name}</span>
</button>
))}
</div>
</div>
)
}
```
```jsx
// components/StageManager.jsx
'use client'
import { useState } from 'react'
export default function StageManager({ stages, setStages, currentStage, setCurrentStage }) {
const [showAddForm, setShowAddForm] = useState(false)
const [newStage, setNewStage] = useState({ name: '', duration: 5 })
const addStage = async (e) => {
e.preventDefault()
if (!newStage.name.trim()) return
const res = await fetch('/api/stages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newStage)
})
if (res.ok) {
setNewStage({ name: '', duration: 5 })
setShowAddForm(false)
window.location.reload()
}
}
const deleteStage = async (id) => {
const res = await fetch(`/api/stages/${id}`, { method: 'DELETE' })
if (res.ok) {
window.location.reload()
}
}
return (
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 shadow-2xl">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-white">📋 مراحل الدرس</h3>
<button
onClick={() => setShowAddForm(!showAddForm)}
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-xl font-bold transition-colors"
>
{showAddForm ? 'إغلاق' : '+ إضافة مرحلة'}
</button>
</div>
{showAddForm && (
<form onSubmit={addStage} className="mb-4 p-4 bg-white/5 rounded-xl">
<div className="grid grid-cols-2 gap-4">
<input
type="text"
value={newStage.name}
onChange={(e) => setNewStage({ ...newStage, name: e.target.value })}
placeholder="اسم المرحلة (مثال: تمهيد)"
className="px-4 py-2 bg-white/10 text-white rounded-xl placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
value={newStage.duration}
onChange={(e) => setNewStage({ ...newStage, duration: parseInt(e.target.value) })}
min="1"
max="60"
placeholder="المدة (دقائق)"
className="px-4 py-2 bg-white/10 text-white rounded-xl placeholder-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
className="mt-4 w-full py-2 bg-green-500 hover:bg-green-600 text-white rounded-xl font-bold transition-colors"
>
حفظ المرحلة
</button>
</form>
)}
{stages.length > 0 ? (
<div className="space-y-3">
{stages.map((stage) => (
<div
key={stage.id}
className={`flex items-center justify-between p-4 rounded-xl transition-colors ${
currentStage?.id === stage.id
? 'bg-yellow-500/20 border-2 border-yellow-500'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div>
<p className="text-white font-bold">{stage.name}</p>
<p className="text-blue-200 text-sm">{stage.duration} دقائق</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setCurrentStage(stage)}
className="px-4 py-2 bg-green-500 hover:bg-green-600 text-white rounded-xl font-bold transition-colors"
>
▶ تشغيل
</button>
<button
onClick={() => deleteStage(stage.id)}
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold transition-colors"
>
🗑 حذف
</button>
</div>
</div>
))}
</div>
) : (
<p className="text-blue-200 text-center py-4">
لا توجد مراحل مضافة. أضف مراحل لتقسيم الدرس.
</p>
)}
</div>
)
}
```
```jsx
// utils/sound.js
let audioContext = null
export const playSound = (type) => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)()
}
const playTone = (frequency, duration, volume = 0.5) => {
const oscillator = audioContext.createOscillator()
const gainNode = audioContext.createGain()
oscillator.connect(gainNode)
gainNode.connect(audioContext.destination)
oscillator.frequency.value = frequency
oscillator.type = 'sine'
gainNode.gain.setValueAtTime(volume, audioContext.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration)
oscillator.start(audioContext.currentTime)
oscillator.stop(audioContext.currentTime + duration)
}
const playSequence = (notes) => {
notes.forEach((note, index) => {
setTimeout(() => {
playTone(note.freq, note.duration, note.volume || 0.5)
}, note.delay || 0)
})
}
switch (type) {
case 'start':
playSequence([
{ freq: 523.25, duration: 0.3, delay: 0 }, // C5
{ freq: 659.25, duration: 0.3, delay: 0.3 }, // E5
{ freq: 783.99, duration: 0.5, delay: 0.6 }, // G5
])
break
case 'lastMinute':
playSequence([
{ freq: 880, duration: 0.1, delay: 0 }, // A5
{ freq: 880, duration: 0.1, delay: 0.2 }, // A5
{ freq: 880, duration: 0.1, delay: 0.4 }, // A5
])
break
case 'timesUp':
playSequence([
{ freq: 392, duration: 0.5, delay: 0 }, // G4
{ freq: 392, duration: 0.5, delay: 0.5 }, // G4
{ freq: 392, duration: 0.5, delay: 1 }, // G4
{ freq: 311.13, duration: 1, delay: 1.5 }, // Eb4
])
break
case 'applause':
// Simulated applause using noise
const bufferSize = audioContext.sampleRate * 2
const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate)
const data = buffer.getChannelData(0)
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1
}
const source = audioContext.createBufferSource()
source.buffer = buffer
const gainNode = audioContext.createGain()
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 2)
source.connect(gainNode)
gainNode.connect(audioContext.destination)
source.start()
break
case 'ding':
playSequence([
{ freq: 1046.5, duration: 0.5, delay: 0 }, // C6
{ freq: 1318.5, duration: 0.5, delay: 0.1 }, // E6
])
break
case 'error':
playSequence([
{ freq: 200, duration: 0.2, delay: 0 },
{ freq: 150, duration: 0.3, delay: 0.2 },
])
break
case 'music':
playSequence([
{ freq: 523.25, duration: 0.2, delay: 0 }, // C5
{ freq: 659.25, duration: 0.2, delay: 0.2 }, // E5
{ freq: 783.99, duration: 0.2, delay: 0.4 }, // G5
{ freq: 1046.5, duration: 0.4, delay: 0.6 }, // C6
{ freq: 783.99, duration: 0.2, delay: 1 }, // G5
{ freq: 659.25, duration: 0.2, delay: 1.2 }, // E5
{ freq: 523.25, duration: 0.4, delay: 1.4 }, // C5
])
break
case 'suspense':
playSequence([
{ freq: 196, duration: 0.3, delay: 0 }, // G3
{ freq: 233.08, duration: 0.3, delay: 0.3 }, // Bb3
{ freq: 261.63, duration: 0.3, delay: 0.6 }, // C4
{ freq: 311.13, duration: 0.3, delay: 0.9 }, // Eb4
{ freq: 349.23, duration: 0.3, delay: 1.2 }, // F4
{ freq: 392, duration: 0.5, delay: 1.5 }, // G4
])
break
default:
break
}
}
```
```jsx
// app/api/students/route.js
import { PrismaClient } from '@prisma/client'
import { NextResponse } from 'next/server'
const prisma = new PrismaClient()
export async function GET() {
const students = await prisma.student.findMany({
orderBy: { name: 'asc' }
})
return NextResponse.json(students)
}
export async function POST(request) {
const { name } = await request.json()
const student = await prisma.student.create({
data: { name }
})
return NextResponse.json(student)
}
```
```jsx
// app/api/stages/route.js
import { PrismaClient } from '@prisma/client'
import { NextResponse } from 'next/server'
const prisma = new PrismaClient()
export async function GET() {
const stages = await prisma.stage.findMany({
orderBy: { createdAt: 'asc' }
})
return NextResponse.json(stages)
}
export async function POST(request) {
const { name, duration } = await request.json()
const stage = await prisma.stage.create({
data: { name, duration }
})
return NextResponse.json(stage)
}
```
```jsx
// app/api/stages/[id]/route.js
import { PrismaClient } from '@prisma/client'
import { NextResponse } from 'next/server'
const prisma = new PrismaClient()
export async function DELETE(request, { params }) {
const { id } = params
await prisma.stage.delete({
where: { id: parseInt(id) }
})
return NextResponse.json({ success: true })
}
```
```jsx
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Student {
id Int @id @default(autoincrement())
name String
}
model Stage {
id Int @id @default(autoincrement())
name String
duration Int
createdAt DateTime @default(now())
}
```
```css
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 30, 58, 138;
--background-end-rgb: 88, 28, 135;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom right,
rgb(var(--background-start-rgb)),
rgb(var(--background-end-rgb))
);
min-height: 100vh;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
```
```json
// package.json
{
"name": "classroom-timer",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"postinstall": "prisma generate && prisma db push"
},
"dependencies": {
"@prisma/client": "^5.6.0",
"next": "14.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"prisma": "^5.6.0",
"tailwindcss": "^3.3.5"
}
}
```
```json
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}
```
```json
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
```
```jsx
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ['@prisma/client', 'prisma'],
},
}
module.exports = nextConfig
```
---
**ملخص التطبيق المكتمل:**
✅ **المؤقت الرئيسي**: دائري مع تغيير الألوان (أخضر، أصفر، أحمر) حسب الوقت المتبقي
✅ **الأصوات التفاعلية**: جرس بداية، تحذير آخر دقيقة، جرس انتهاء، تصفيق، جرس نجاح، صوت خطأ، موسيقى
✅ **اختيار الطالب العشوائي**: مع تأثير تشويقي وصوت
✅ **إدارة المراحل**: إضافة/حذف/تشغيل مراحل الدرس بشكل منفصل
✅ **التحكم بالصوت**: كتم/تشغيل كامل
✅ **تصميم عربي RTL**: بألوان مريحة وخط Cairo
✅ **قاعدة بيانات**: لتخزين الطلاب والمراحل بشكل دائم
**للتشغيل محلياً:**
```bash
npm install
npx prisma db push
npm run dev
```
**للنشر على Vercel:**
```bash
npm run build
vercel deploy
```
التطبيق جاهز للاستخدام مباشرة في الفصل الدراسي! 🎓
Entre para ver o prompt completo
Continuar com:
Ao entrar, você concorda com nossos Termos de uso e Política de privacidade
Uso
Este prompt foi projetado para uso com education. Copie o conteúdo acima e cole na sua ferramenta de IA preferida.
Para melhores resultados, personalize os marcadores (indicados por colchetes ou maiúsculas) com seus requisitos específicos.
Discussão
0 comentários